From 6b4c3914b082a450c599d228508a56259d910027 Mon Sep 17 00:00:00 2001 From: Rui Date: Thu, 1 Aug 2024 14:11:37 -0700 Subject: [PATCH 01/63] WIP --- .../exchange.dydx.abacus/output/Market.kt | 91 ++++---- .../processor/markets/MarketProcessor.kt | 207 +++++++++++++++++- .../processor/markets/MarketsProcessor.kt | 76 ++++++- .../markets/MarketsSummaryProcessor.kt | 38 +++- .../state/internalstate/InternalState.kt | 16 +- .../model/TradingStateMachine+Markets.kt | 51 +++-- .../state/model/TradingStateMachine+Trades.kt | 8 +- .../state/model/TradingStateMachine.kt | 10 +- .../models/IndexerCompositeMarketObject.kt | 75 +++++++ .../models/IndexerWsMarketUpdateResponse.kt | 18 ++ 10 files changed, 491 insertions(+), 99 deletions(-) create mode 100644 src/commonMain/kotlin/indexer/models/IndexerCompositeMarketObject.kt create mode 100644 src/commonMain/kotlin/indexer/models/IndexerWsMarketUpdateResponse.kt diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/output/Market.kt b/src/commonMain/kotlin/exchange.dydx.abacus/output/Market.kt index db5e796f0..bf2adfffa 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/output/Market.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/output/Market.kt @@ -6,6 +6,7 @@ import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.changes.Changes import exchange.dydx.abacus.state.changes.StateChanges +import exchange.dydx.abacus.state.internalstate.InternalMarketSummaryState import exchange.dydx.abacus.state.manager.OrderbookGrouping import exchange.dydx.abacus.utils.IList import exchange.dydx.abacus.utils.IMap @@ -50,6 +51,9 @@ data class MarketStatus( } } } + + val canDisplay: Boolean + get() = canTrade || canReduce } /* for V4 only */ @@ -942,63 +946,50 @@ data class PerpetualMarketSummary( val markets: IMap?, ) { companion object { - internal fun create( - existing: PerpetualMarketSummary?, - parser: ParserProtocol, - data: Map, - assets: Map? - ): PerpetualMarketSummary? { - Logger.d { "creating Perpetual Market Summary\n" } - - val markets: IMutableMap = - iMutableMapOf() - val marketsData = parser.asMap(data["markets"]) ?: return null - - for ((key, value) in marketsData) { - val marketData = parser.asMap(value) ?: iMapOf() - PerpetualMarket.create( - existing?.markets?.get(key), - parser, - marketData, - assets, - false, - false, - ) - ?.let { market -> - markets[key] = market - } - } - - return perpetualMarketSummary(existing, parser, data, markets) - } - internal fun apply( existing: PerpetualMarketSummary?, parser: ParserProtocol, data: Map, assets: Map?, + staticTyping: Boolean, + marketSummaryState: InternalMarketSummaryState, changes: StateChanges, ): PerpetualMarketSummary? { - val marketsData = parser.asMap(data["markets"]) ?: return null - val changedMarkets = changes.markets ?: marketsData.keys + if (staticTyping) { + if (marketSummaryState.markets.isEmpty()) { + return null + } + val changedMarkets = changes.markets ?: marketSummaryState.markets.keys + val markets = existing?.markets?.mutable() ?: iMutableMapOf() + for (marketId in changedMarkets) { + val perpetualMarket = marketSummaryState.markets[marketId]?.perpetualMarket + if (perpetualMarket != null) { + markets[marketId] = perpetualMarket + } + } + return perpetualMarketSummary(existing, parser, data, markets) + } else { + val marketsData = parser.asMap(data["markets"]) ?: return null + val changedMarkets = changes.markets ?: marketsData.keys - val markets = existing?.markets?.mutable() ?: iMutableMapOf() - for (marketId in changedMarkets) { - val marketData = parser.asMap(marketsData[marketId]) ?: continue + val markets = existing?.markets?.mutable() ?: iMutableMapOf() + for (marketId in changedMarkets) { + val marketData = parser.asMap(marketsData[marketId]) ?: continue // val marketData = parser.asMap(configDataMap["configs"]) ?: continue - val existingMarket = existing?.markets?.get(marketId) + val existingMarket = existing?.markets?.get(marketId) - val perpMarket = PerpetualMarket.create( - existingMarket, - parser, - marketData, - assets, - changes.changes.contains(Changes.orderbook), - changes.changes.contains(Changes.trades), - ) - markets.typedSafeSet(marketId, perpMarket) + val perpMarket = PerpetualMarket.create( + existing = existingMarket, + parser = parser, + data = marketData, + assets = assets, + resetOrderbook = changes.changes.contains(Changes.orderbook), + resetTrades = changes.changes.contains(Changes.trades), + ) + markets.typedSafeSet(marketId, perpMarket) + } + return perpetualMarketSummary(existing, parser, data, markets) } - return perpetualMarketSummary(existing, parser, data, markets) } private fun perpetualMarketSummary( @@ -1020,10 +1011,10 @@ data class PerpetualMarketSummary( existing } else { PerpetualMarketSummary( - volume24HUSDC, - openInterestUSDC, - trades24H, - newMarkets, + volume24HUSDC = volume24HUSDC, + openInterestUSDC = openInterestUSDC, + trades24H = trades24H, + markets = newMarkets, ) } } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketProcessor.kt index 2c293fedf..a5740df08 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketProcessor.kt @@ -1,12 +1,26 @@ package exchange.dydx.abacus.processor.markets +import exchange.dydx.abacus.output.MarketConfigs +import exchange.dydx.abacus.output.MarketConfigsV4 +import exchange.dydx.abacus.output.MarketPerpetual +import exchange.dydx.abacus.output.MarketStatus +import exchange.dydx.abacus.output.PerpetualMarket +import exchange.dydx.abacus.output.PerpetualMarketType import exchange.dydx.abacus.processor.base.BaseProcessor import exchange.dydx.abacus.processor.utils.MarketId import exchange.dydx.abacus.protocols.ParserProtocol +import exchange.dydx.abacus.utils.IndexerResponseParsingException +import exchange.dydx.abacus.utils.Logger import exchange.dydx.abacus.utils.Numeric import exchange.dydx.abacus.utils.ServerTime import exchange.dydx.abacus.utils.mutable +import exchange.dydx.abacus.utils.parseException import exchange.dydx.abacus.utils.safeSet +import indexer.codegen.IndexerPerpetualMarketStatus +import indexer.codegen.IndexerPerpetualMarketType +import indexer.models.IndexerCompositeMarketObject +import indexer.models.IndexerWsMarketOraclePriceObject +import numberOfDecimals import kotlin.math.max import kotlin.math.min import kotlin.time.Duration.Companion.seconds @@ -127,6 +141,164 @@ internal class MarketProcessor( ), ) + private var cachedIndexerMarketResponse: IndexerCompositeMarketObject? = null + private var cachedIndexerOraclePrice: IndexerWsMarketOraclePriceObject? = null + + fun process( + payload: IndexerCompositeMarketObject, + ): PerpetualMarket? { + cachedIndexerMarketResponse = if (cachedIndexerMarketResponse != null) { + cachedIndexerMarketResponse?.copyNotNulls(payload) + } else { + payload + } + return createPerpetualMarket() + } + + fun processOraclePrice( + payload: IndexerWsMarketOraclePriceObject + ): PerpetualMarket? { + cachedIndexerOraclePrice = payload + return createPerpetualMarket() + } + + private fun createPerpetualMarket(): PerpetualMarket? { + val payload = cachedIndexerMarketResponse ?: return null + val name = parser.asString(payload.ticker) ?: return null + val oraclePrice = parser.asDouble(cachedIndexerOraclePrice?.oraclePrice) ?: parser.asDouble(payload.oraclePrice) + val status = createStatus(payload.status) + if (status == null || !status.canDisplay) { + return null + } + try { + val newValue = PerpetualMarket( + id = name, + assetId = MarketId.assetid(name) ?: parseException(payload), + oraclePrice = oraclePrice, + market = name, + marketCaps = null, + priceChange24H = parser.asDouble(payload.priceChange24H), + priceChange24HPercent = calculatePriceChange24HPercent( + parser.asDouble(payload.priceChange24H), + oraclePrice, + ), + status = status, + configs = createConfigs(payload), + perpetual = createMarketPerpetual(payload), + ) + return newValue + } catch (e: IndexerResponseParsingException) { + Logger.e { "${e.message}" } + return null + } + } + + private fun createStatus( + indexerStatus: IndexerPerpetualMarketStatus? + ): MarketStatus? { + return when (indexerStatus) { + IndexerPerpetualMarketStatus.ACTIVE -> MarketStatus( + canTrade = true, + canReduce = true, + ) + IndexerPerpetualMarketStatus.CANCELONLY -> MarketStatus( + canTrade = false, + canReduce = true, + ) + null -> null + else -> MarketStatus( + canTrade = false, + canReduce = false, + ) + } + } + + private fun createConfigs( + payload: IndexerCompositeMarketObject, + ): MarketConfigs { + val stepSize = parser.asDouble(payload.stepSize) + val tickSize = parser.asDouble(payload.tickSize) + return MarketConfigs( + clobPairId = payload.clobPairId, + largeSize = null, + stepSize = stepSize, + tickSize = tickSize, + stepSizeDecimals = stepSize?.numberOfDecimals(), + tickSizeDecimals = tickSize?.numberOfDecimals(), + displayStepSize = stepSize, + displayTickSize = tickSize, + displayStepSizeDecimals = stepSize?.numberOfDecimals(), + displayTickSizeDecimals = tickSize?.numberOfDecimals(), + effectiveInitialMarginFraction = calculateEffectiveInitialMarginFraction( + baseIMF = parser.asDouble(payload.initialMarginFraction), + openInterest = parser.asDouble(payload.openInterest), + openInterestLowerCap = parser.asDouble(payload.openInterestLowerCap), + openInterestUpperCap = parser.asDouble(payload.openInterestUpperCap), + oraclePrice = parser.asDouble(payload.oraclePrice), + ), + minOrderSize = stepSize, + initialMarginFraction = parser.asDouble(payload.initialMarginFraction), + maintenanceMarginFraction = parser.asDouble(payload.maintenanceMarginFraction), + incrementalInitialMarginFraction = null, + incrementalPositionSize = parser.asDouble(payload.incrementalPositionSize), + maxPositionSize = parser.asDouble(payload.maxPositionSize), + basePositionNotional = null, + baselinePositionSize = parser.asDouble(payload.basePositionSize), + candleOptions = null, + perpetualMarketType = when (payload.marketType) { + IndexerPerpetualMarketType.CROSS -> PerpetualMarketType.CROSS + IndexerPerpetualMarketType.ISOLATED -> PerpetualMarketType.ISOLATED + else -> PerpetualMarketType.CROSS + }, + v4 = createConfigsV4(payload), + ) + } + + private fun createConfigsV4( + payload: IndexerCompositeMarketObject, + ): MarketConfigsV4? { + val clobPairId = parser.asInt(payload.clobPairId) + val atomicResolution = parser.asInt(payload.atomicResolution) + val stepBaseQuantums = parser.asInt(payload.stepBaseQuantums) + val quantumConversionExponent = parser.asInt(payload.quantumConversionExponent) + val subticksPerTick = parser.asInt(payload.subticksPerTick) + return if (clobPairId != null && atomicResolution != null && stepBaseQuantums != null && quantumConversionExponent != null && subticksPerTick != null) { + MarketConfigsV4( + clobPairId = clobPairId, + atomicResolution = atomicResolution, + stepBaseQuantums = stepBaseQuantums, + quantumConversionExponent = quantumConversionExponent, + subticksPerTick = subticksPerTick, + ) + } else { + null + } + } + + private fun createMarketPerpetual( + payload: IndexerCompositeMarketObject, + ): MarketPerpetual? { + val nextFundingRate = parser.asDouble(payload.nextFundingRate) + val openInterest = parser.asDouble(payload.openInterest) + val oraclePrice = parser.asDouble(payload.oraclePrice) + return if (openInterest != null) { + MarketPerpetual( + volume24H = parser.asDouble(payload.volume24H), + trades24H = parser.asDouble(payload.trades24H), + volume24HUSDC = null, + nextFundingRate = nextFundingRate, + nextFundingAtMilliseconds = null, + openInterest = openInterest, + openInterestUSDC = oraclePrice?.let { openInterest * it } ?: 0.0, + openInterestLowerCap = parser.asDouble(payload.openInterestLowerCap), + openInterestUpperCap = parser.asDouble(payload.openInterestUpperCap), + line = null + ) + } else { + null + } + } + override fun received( existing: Map?, payload: Map, @@ -153,14 +325,22 @@ internal class MarketProcessor( return calculate(output) } - internal fun effectiveInitialMarginFraction(output: Map, oraclePrice: Double?): Double? { + private fun effectiveInitialMarginFraction(output: Map, oraclePrice: Double?): Double? { val baseIMF = parser.asDouble(parser.value(output, "configs.initialMarginFraction")) val openInterest = parser.asDouble(parser.value(output, "perpetual.openInterest")) val openInterestLowerCap = parser.asDouble(parser.value(output, "perpetual.openInterestLowerCap")) val openInterestUpperCap = parser.asDouble(parser.value(output, "perpetual.openInterestUpperCap")) - // need nully checks because all properties are optional in the websocket message - // clean up after https://linear.app/dydx/issue/OTE-301/audit-websocket-message-types-in-indexer is done + return calculateEffectiveInitialMarginFraction(baseIMF, openInterest, openInterestLowerCap, openInterestUpperCap, oraclePrice) + } + + private fun calculateEffectiveInitialMarginFraction( + baseIMF: Double?, + openInterest: Double?, + openInterestLowerCap: Double?, + openInterestUpperCap: Double?, + oraclePrice: Double?, + ): Double? { if (baseIMF === null) return null if (oraclePrice == null || openInterest == null || openInterestLowerCap == null || openInterestUpperCap == null) return baseIMF // if these are equal we can throw an error from dividing by zero @@ -173,7 +353,7 @@ internal class MarketProcessor( return effectiveIMF } - internal fun receivedDelta( + internal fun receivedDeltaDeprecated( market: Map?, payload: Map, ): Map { @@ -193,17 +373,24 @@ internal class MarketProcessor( val modified = market.mutable() modified.safeSet( "priceChange24HPercent", - if (priceChange24H != null && oraclePrice != null && oraclePrice > priceChange24H) { - val basePrice = (oraclePrice - priceChange24H) - if (basePrice > Numeric.double.ZERO) (priceChange24H / basePrice) else null - } else { - null - }, + calculatePriceChange24HPercent(priceChange24H, oraclePrice), ) return modified } + private fun calculatePriceChange24HPercent( + priceChange24H: Double?, + oraclePrice: Double?, + ): Double? { + return if (priceChange24H != null && oraclePrice != null && oraclePrice > priceChange24H) { + val basePrice = (oraclePrice - priceChange24H) + if (basePrice > Numeric.double.ZERO) (priceChange24H / basePrice) else null + } else { + null + } + } + internal fun receivedConfigurations( market: Map?, payload: Map, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsProcessor.kt index 99eb702dc..a40456012 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsProcessor.kt @@ -1,12 +1,16 @@ package exchange.dydx.abacus.processor.markets +import exchange.dydx.abacus.output.PerpetualMarket import exchange.dydx.abacus.processor.base.BaseProcessor import exchange.dydx.abacus.protocols.ParserProtocol +import exchange.dydx.abacus.state.internalstate.InternalMarketState +import exchange.dydx.abacus.state.internalstate.InternalMarketSummaryState import exchange.dydx.abacus.utils.Logger import exchange.dydx.abacus.utils.mutable import exchange.dydx.abacus.utils.safeSet +import indexer.models.IndexerCompositeMarketObject +import indexer.models.IndexerWsMarketUpdateResponse -@Suppress("UNCHECKED_CAST") internal class MarketsProcessor( parser: ParserProtocol, calculateSparklines: Boolean @@ -19,7 +23,65 @@ internal class MarketsProcessor( marketProcessor.groupingMultiplier = value } - internal fun processSubscribed( + fun processSubscribed( + existing: InternalMarketSummaryState, + content: Map? + ): InternalMarketSummaryState { + for ((marketId, marketData) in content ?: mapOf()) { + val receivedMarket = marketProcessor.process( + payload = marketData + ) + val marketState = existing.markets[marketId] ?: InternalMarketState() + marketState.perpetualMarket = receivedMarket + existing.markets[marketId] = marketState + } + return existing + } + + fun processChannelData( + existing:InternalMarketSummaryState, + content: IndexerWsMarketUpdateResponse?, + ): InternalMarketSummaryState { + if (content != null) { + if (!content.trading.isNullOrEmpty()) { + for ((marketId, marketData) in content.trading) { + val marketState = existing.markets[marketId] ?: InternalMarketState() + val receivedMarket = marketProcessor.process( + payload = marketData + ) + if (receivedMarket != marketState.perpetualMarket) { + marketState.perpetualMarket = receivedMarket + existing.markets[marketId] = marketState + } + } + } + if (!content.oraclePrices.isNullOrEmpty()) { + for ((marketId, oracleData) in content.oraclePrices) { + val marketState = existing.markets[marketId] ?: InternalMarketState() + val receivedMarket = marketProcessor.processOraclePrice( + payload = oracleData + ) + if (receivedMarket != marketState.perpetualMarket) { + marketState.perpetualMarket = receivedMarket + existing.markets[marketId] = marketState + } + } + } + } + return existing + } + + fun processChannelBatchData( + existing:InternalMarketSummaryState, + content: List?, + ): InternalMarketSummaryState { + for (response in content ?: listOf()) { + processChannelData(existing, response) + } + return existing + } + + internal fun processSubscribedDeprecated( existing: Map?, content: Map ): Map? { @@ -35,17 +97,17 @@ internal class MarketsProcessor( existing: Map?, content: Map ): Map { - return receivedChanges(existing, content) + return receivedChangesDeprecated(existing, content) } - internal fun processChannelBatchData( + internal fun processChannelBatchDataDeprecated( existing: Map?, content: List ): Map { var data = existing ?: mapOf() for (partialPayload in content) { parser.asNativeMap(partialPayload)?.let { - data = receivedChanges(data, it) + data = receivedChangesDeprecated(data, it) } } return data @@ -69,7 +131,7 @@ internal class MarketsProcessor( return markets } - private fun receivedChanges( + private fun receivedChangesDeprecated( existing: Map?, payload: Map ): Map { @@ -78,7 +140,7 @@ internal class MarketsProcessor( for ((market, data) in narrowedPayload) { val marketPayload = parser.asNativeMap(data) if (marketPayload != null) { - val receivedMarket = marketProcessor.receivedDelta( + val receivedMarket = marketProcessor.receivedDeltaDeprecated( parser.asNativeMap(existing?.get(market)), marketPayload, ) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsSummaryProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsSummaryProcessor.kt index 857720e56..b66a40d16 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsSummaryProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsSummaryProcessor.kt @@ -2,10 +2,12 @@ package exchange.dydx.abacus.processor.markets import exchange.dydx.abacus.processor.base.BaseProcessor import exchange.dydx.abacus.protocols.ParserProtocol +import exchange.dydx.abacus.state.internalstate.InternalMarketSummaryState import exchange.dydx.abacus.utils.mutable import exchange.dydx.abacus.utils.safeSet +import indexer.models.IndexerCompositeMarketObject +import indexer.models.IndexerWsMarketUpdateResponse -@Suppress("UNCHECKED_CAST") internal class MarketsSummaryProcessor( parser: ParserProtocol, calculateSparklines: Boolean = false @@ -18,16 +20,40 @@ internal class MarketsSummaryProcessor( marketsProcessor.groupingMultiplier = value } - internal fun subscribed( + fun processSubscribed( + existing: InternalMarketSummaryState, + content: Map?, + ): InternalMarketSummaryState { + val markets = marketsProcessor.processSubscribed(existing, content) + return existing + } + + fun processChannelData( + existing: InternalMarketSummaryState, + content: IndexerWsMarketUpdateResponse?, + ): InternalMarketSummaryState { + val markets = marketsProcessor.processChannelData(existing, content) + return existing + } + + fun processChannelBatchData( + existing: InternalMarketSummaryState, + content: List?, + ): InternalMarketSummaryState { + val markets = marketsProcessor.processChannelBatchData(existing, content) + return existing + } + + internal fun subscribedDeprecated( existing: Map?, content: Map ): Map? { - val markets = marketsProcessor.processSubscribed(parser.asNativeMap(existing?.get("markets")), content) + val markets = marketsProcessor.processSubscribedDeprecated(parser.asNativeMap(existing?.get("markets")), content) return modify(existing, markets) } @Suppress("FunctionName") - internal fun channel_data( + internal fun channel_dataDeprecated( existing: Map?, content: Map ): Map? { @@ -36,12 +62,12 @@ internal class MarketsSummaryProcessor( } @Suppress("FunctionName") - internal fun channel_batch_data( + internal fun channel_batch_dataDeprecated( existing: Map?, content: List ): Map? { val markets = - marketsProcessor.processChannelBatchData(parser.asNativeMap(existing?.get("markets")), content) + marketsProcessor.processChannelBatchDataDeprecated(parser.asNativeMap(existing?.get("markets")), content) return modify(existing, markets) } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt index b2ef48e57..0992e1285 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt @@ -6,6 +6,7 @@ import exchange.dydx.abacus.output.Asset import exchange.dydx.abacus.output.LaunchIncentivePoint import exchange.dydx.abacus.output.LaunchIncentiveSeason import exchange.dydx.abacus.output.MarketTrade +import exchange.dydx.abacus.output.PerpetualMarket import exchange.dydx.abacus.output.account.PositionSide import exchange.dydx.abacus.output.account.StakingRewards import exchange.dydx.abacus.output.account.SubaccountFill @@ -26,7 +27,16 @@ internal data class InternalState( val wallet: InternalWalletState = InternalWalletState(), var rewardsParams: InternalRewardsParamsState? = null, val launchIncentive: InternalLaunchIncentiveState = InternalLaunchIncentiveState(), - val markets: MutableMap = mutableMapOf(), + val marketsSummary: InternalMarketSummaryState = InternalMarketSummaryState(), +) + +internal data class InternalMarketSummaryState( + var markets: MutableMap = mutableMapOf(), +) + +internal data class InternalMarketState( + var trades: List? = null, + var perpetualMarket: PerpetualMarket? = null, ) internal data class InternalWalletState( @@ -154,7 +164,3 @@ internal data class InternalRewardsParamsState( internal data class InternalLaunchIncentiveState( var seasons: List? = null, ) - -internal data class InternalMarketState( - var trades: List? = null, -) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Markets.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Markets.kt index 3ae9a7b3c..208fb1490 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Markets.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Markets.kt @@ -1,8 +1,13 @@ package exchange.dydx.abacus.state.model import exchange.dydx.abacus.calculator.MarginCalculator +import exchange.dydx.abacus.protocols.asTypedList +import exchange.dydx.abacus.protocols.asTypedObject +import exchange.dydx.abacus.protocols.asTypedStringMap import exchange.dydx.abacus.state.changes.Changes import exchange.dydx.abacus.state.changes.StateChanges +import indexer.models.IndexerCompositeMarketObject +import indexer.models.IndexerWsMarketUpdateResponse import indexer.models.configs.AssetJson import kollections.iListOf import kollections.toIList @@ -11,25 +16,31 @@ internal fun TradingStateMachine.receivedMarkets( payload: Map, subaccountNumber: Int, ): StateChanges { - marketsSummary = marketsProcessor.subscribed(marketsSummary, payload) + if (staticTyping) { + val markets = parser.asNativeMap(payload.get("markets")) + val marketsPayload = parser.asTypedStringMap(markets) + marketsProcessor.processSubscribed(internalState.marketsSummary, marketsPayload) + } + // TODO remove deprecated + marketsSummary = marketsProcessor.subscribedDeprecated(marketsSummary, payload) marketsSummary = marketsCalculator.calculate(parser.asMap(marketsSummary), assets, null) val subaccountNumbers = MarginCalculator.getChangedSubaccountNumbers( - parser, - account, - subaccountNumber, - parser.asMap(input?.get("trade")), + parser = parser, + account = account, + subaccountNumber = subaccountNumber, + tradeInput = parser.asMap(input?.get("trade")), ) return StateChanges( - iListOf( + changes = iListOf( Changes.assets, Changes.markets, Changes.subaccount, Changes.input, Changes.historicalPnl, ), - null, - subaccountNumbers, + markets = null, + subaccountNumbers = subaccountNumbers, ) } @@ -37,8 +48,11 @@ internal fun TradingStateMachine.receivedMarketsChanges( payload: Map, subaccountNumber: Int, ): StateChanges { - val blankAssets = assets == null - marketsSummary = marketsProcessor.channel_data(marketsSummary, payload) + if (staticTyping) { + val response = parser.asTypedObject(payload) + marketsProcessor.processChannelData(internalState.marketsSummary, response) + } + marketsSummary = marketsProcessor.channel_dataDeprecated(marketsSummary, payload) marketsSummary = marketsCalculator.calculate(marketsSummary, assets, payload.keys) val subaccountNumbers = MarginCalculator.getChangedSubaccountNumbers( parser, @@ -47,6 +61,7 @@ internal fun TradingStateMachine.receivedMarketsChanges( parser.asMap(input?.get("trade")), ) + val blankAssets = assets == null return StateChanges( if (blankAssets) { iListOf( @@ -73,8 +88,11 @@ internal fun TradingStateMachine.receivedBatchedMarketsChanges( payload: List, subaccountNumber: Int, ): StateChanges { - val blankAssets = assets == null - marketsSummary = marketsProcessor.channel_batch_data(marketsSummary, payload) + if (staticTyping) { + val response = parser.asTypedList(payload) + marketsProcessor.processChannelBatchData(internalState.marketsSummary, response) + } + marketsSummary = marketsProcessor.channel_batch_dataDeprecated(marketsSummary, payload) val keys = mutableSetOf() for (partialPayload in payload) { parser.asMap(partialPayload)?.let { partialPayload -> @@ -86,12 +104,13 @@ internal fun TradingStateMachine.receivedBatchedMarketsChanges( } marketsSummary = marketsCalculator.calculate(marketsSummary, assets, keys) val subaccountNumbers = MarginCalculator.getChangedSubaccountNumbers( - parser, - account, - subaccountNumber, - parser.asMap(input?.get("trade")), + parser = parser, + account = account, + subaccountNumber = subaccountNumber, + tradeInput = parser.asMap(input?.get("trade")), ) + val blankAssets = assets == null return StateChanges( if (blankAssets) { iListOf( diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Trades.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Trades.kt index 43362ddff..380f4f470 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Trades.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Trades.kt @@ -12,12 +12,12 @@ internal fun TradingStateMachine.receivedTrades( return if (market != null) { this.marketsSummary = marketsProcessor.receivedTradesDeprecated(marketsSummary, market, payload) if (staticTyping) { - val marketState = internalState.markets[market] + val marketState = internalState.marketsSummary.markets[market] val trades = tradesProcessorV2.processSubscribed(payload) if (marketState != null) { marketState.trades = trades } else { - internalState.markets[market] = InternalMarketState(trades) + internalState.marketsSummary.markets[market] = InternalMarketState(trades) } } StateChanges(iListOf(Changes.trades), iListOf(market)) @@ -33,12 +33,12 @@ internal fun TradingStateMachine.receivedTradesChanges( return if (market != null) { this.marketsSummary = marketsProcessor.receivedTradesChangesDeprecated(marketsSummary, market, payload) if (staticTyping) { - val marketState = internalState.markets[market] + val marketState = internalState.marketsSummary.markets[market] val trades = tradesProcessorV2.processChannelData(marketState?.trades, payload) if (marketState != null) { marketState.trades = trades } else { - internalState.markets[market] = InternalMarketState(trades) + internalState.marketsSummary.markets[market] = InternalMarketState(trades) } } StateChanges(iListOf(Changes.trades), iListOf(market)) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt index 2b9f83557..d7ab657ee 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt @@ -1091,7 +1091,15 @@ open class TradingStateMachine( if (changes.changes.contains(Changes.markets)) { parser.asNativeMap(data?.get("markets"))?.let { marketsSummary = - PerpetualMarketSummary.apply(marketsSummary, parser, it, this.assets, changes) + PerpetualMarketSummary.apply( + existing = marketsSummary, + parser = parser, + data = it, + assets = this.assets, + staticTyping = staticTyping, + marketSummaryState = internalState.marketsSummary, + changes = changes + ) } ?: run { marketsSummary = null } diff --git a/src/commonMain/kotlin/indexer/models/IndexerCompositeMarketObject.kt b/src/commonMain/kotlin/indexer/models/IndexerCompositeMarketObject.kt new file mode 100644 index 000000000..b0175ae94 --- /dev/null +++ b/src/commonMain/kotlin/indexer/models/IndexerCompositeMarketObject.kt @@ -0,0 +1,75 @@ +package indexer.models + +import indexer.codegen.IndexerPerpetualMarketStatus +import indexer.codegen.IndexerPerpetualMarketType +import kotlinx.serialization.Serializable + +@Serializable +data class IndexerCompositeMarketObject( + + // Copied from IndexerPerpetualMarketResponseObject + + val clobPairId: kotlin.String? = null, + val ticker: kotlin.String? = null, + val status: IndexerPerpetualMarketStatus? = null, + val oraclePrice: kotlin.String? = null, + val priceChange24H: kotlin.String? = null, + val volume24H: kotlin.String? = null, + val trades24H: kotlin.Int? = null, + val nextFundingRate: kotlin.String? = null, + val initialMarginFraction: kotlin.String? = null, + val maintenanceMarginFraction: kotlin.String? = null, + val openInterest: kotlin.String? = null, + val atomicResolution: kotlin.Int? = null, + val quantumConversionExponent: kotlin.Int? = null, + val tickSize: kotlin.String? = null, + val stepSize: kotlin.String? = null, + val stepBaseQuantums: kotlin.Int? = null, + val subticksPerTick: kotlin.Int? = null, + val marketType: IndexerPerpetualMarketType? = null, + val openInterestLowerCap: kotlin.String? = null, + val openInterestUpperCap: kotlin.String? = null, + val baseOpenInterest: kotlin.String? = null, + + // Additional fields for WS + val id: kotlin.String? = null, + val marketId: kotlin.Int? = null, + val baseAsset: kotlin.String? = null, + val quoteAsset: kotlin.String? = null, + val basePositionSize: kotlin.String? = null, + val incrementalPositionSize: kotlin.String? = null, + val maxPositionSize: kotlin.String? = null, +) { + fun copyNotNulls(from: IndexerCompositeMarketObject): IndexerCompositeMarketObject { + return IndexerCompositeMarketObject( + clobPairId = from.clobPairId ?: clobPairId, + ticker = from.ticker ?: ticker, + status = from.status ?: status, + oraclePrice = from.oraclePrice ?: oraclePrice, + priceChange24H = from.priceChange24H ?: priceChange24H, + volume24H = from.volume24H ?: volume24H, + trades24H = from.trades24H ?: trades24H, + nextFundingRate = from.nextFundingRate ?: nextFundingRate, + initialMarginFraction = from.initialMarginFraction ?: initialMarginFraction, + maintenanceMarginFraction = from.maintenanceMarginFraction ?: maintenanceMarginFraction, + openInterest = from.openInterest ?: openInterest, + atomicResolution = from.atomicResolution ?: atomicResolution, + quantumConversionExponent = from.quantumConversionExponent ?: quantumConversionExponent, + tickSize = from.tickSize ?: tickSize, + stepSize = from.stepSize ?: stepSize, + stepBaseQuantums = from.stepBaseQuantums ?: stepBaseQuantums, + subticksPerTick = from.subticksPerTick ?: subticksPerTick, + marketType = from.marketType ?: marketType, + openInterestLowerCap = from.openInterestLowerCap ?: openInterestLowerCap, + openInterestUpperCap = from.openInterestUpperCap ?: openInterestUpperCap, + baseOpenInterest = from.baseOpenInterest ?: baseOpenInterest, + id = from.id ?: id, + marketId = from.marketId ?: marketId, + baseAsset = from.baseAsset ?: baseAsset, + quoteAsset = from.quoteAsset ?: quoteAsset, + basePositionSize = from.basePositionSize ?: basePositionSize, + incrementalPositionSize = from.incrementalPositionSize ?: incrementalPositionSize, + maxPositionSize = from.maxPositionSize ?: maxPositionSize, + ) + } +} diff --git a/src/commonMain/kotlin/indexer/models/IndexerWsMarketUpdateResponse.kt b/src/commonMain/kotlin/indexer/models/IndexerWsMarketUpdateResponse.kt new file mode 100644 index 000000000..2b002719e --- /dev/null +++ b/src/commonMain/kotlin/indexer/models/IndexerWsMarketUpdateResponse.kt @@ -0,0 +1,18 @@ +package indexer.models + +import indexer.codegen.IndexerIsoString +import kotlinx.serialization.Serializable + +@Serializable +data class IndexerWsMarketUpdateResponse( + val trading: Map? = null, + val oraclePrices: Map? = null, +) + +@Serializable +data class IndexerWsMarketOraclePriceObject( + val oraclePrice: kotlin.String? = null, + val effectiveAt: IndexerIsoString? = null, + val effectiveAtHeight: kotlin.String? = null, + val marketId: kotlin.Int? = null, +) \ No newline at end of file From 252219160258a9851a7babc7f61e639ffdf29cec Mon Sep 17 00:00:00 2001 From: Rui Date: Fri, 2 Aug 2024 11:22:46 -0700 Subject: [PATCH 02/63] MarketProcessor --- .../exchange.dydx.abacus/output/Market.kt | 16 +- .../processor/assets/AssetProcessor.kt | 7 +- .../processor/assets/AssetsProcessor.kt | 4 +- .../processor/markets/MarketProcessor.kt | 52 +++++-- .../processor/markets/MarketsProcessor.kt | 77 +++++---- .../markets/MarketsSummaryProcessor.kt | 6 +- .../state/internalstate/InternalState.kt | 4 +- .../model/TradingStateMachine+Markets.kt | 11 +- .../state/model/TradingStateMachine.kt | 4 +- .../models/IndexerCompositeMarketObject.kt | 4 + ...tJson.kt => ConfigsMarketAssetResponse.kt} | 2 +- .../exchange.dydx.abacus/payload/BaseTests.kt | 18 ++- .../processor/markets/MarketProcessorTests.kt | 146 ++++++++++++++++++ .../markets/MarketsProcessorTests.kt | 85 ++++++++++ .../processor/markets/MarketProcessorMock.kt | 40 +++++ .../tests/payloads/MarketsChannelMock.kt | 22 +-- 16 files changed, 408 insertions(+), 90 deletions(-) rename src/commonMain/kotlin/indexer/models/configs/{AssetJson.kt => ConfigsMarketAssetResponse.kt} (92%) create mode 100644 src/commonTest/kotlin/exchange.dydx.abacus/processor/markets/MarketProcessorTests.kt create mode 100644 src/commonTest/kotlin/exchange.dydx.abacus/processor/markets/MarketsProcessorTests.kt create mode 100644 src/commonTest/kotlin/exchange.dydx.abacus/tests/mock/processor/markets/MarketProcessorMock.kt diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/output/Market.kt b/src/commonMain/kotlin/exchange.dydx.abacus/output/Market.kt index 81ff25da0..ca4397497 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/output/Market.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/output/Market.kt @@ -10,7 +10,6 @@ import exchange.dydx.abacus.state.internalstate.InternalMarketSummaryState import exchange.dydx.abacus.state.manager.OrderbookGrouping import exchange.dydx.abacus.utils.IList import exchange.dydx.abacus.utils.IMap -import exchange.dydx.abacus.utils.IMutableMap import exchange.dydx.abacus.utils.Logger import exchange.dydx.abacus.utils.ParsingHelper import exchange.dydx.abacus.utils.mutable @@ -19,6 +18,7 @@ import kollections.JsExport import kollections.iMutableListOf import kollections.iMutableMapOf import kollections.toIList +import kollections.toIMap import kotlinx.serialization.Serializable import numberOfDecimals @@ -958,12 +958,10 @@ data class PerpetualMarketSummary( if (marketSummaryState.markets.isEmpty()) { return null } - val changedMarkets = changes.markets ?: marketSummaryState.markets.keys - val markets = existing?.markets?.mutable() ?: iMutableMapOf() - for (marketId in changedMarkets) { - val perpetualMarket = marketSummaryState.markets[marketId]?.perpetualMarket - if (perpetualMarket != null) { - markets[marketId] = perpetualMarket + val markets: MutableMap = mutableMapOf() + for ((marketId, market) in marketSummaryState.markets) { + market.perpetualMarket?.let { + markets[marketId] = it } } return perpetualMarketSummary(existing, parser, data, markets) @@ -995,7 +993,7 @@ data class PerpetualMarketSummary( existing: PerpetualMarketSummary?, parser: ParserProtocol, data: Map, - newMarkets: IMutableMap, + newMarkets: Map, ): PerpetualMarketSummary? { val volume24HUSDC = parser.asDouble(data["volume24HUSDC"]) val openInterestUSDC = parser.asDouble(data["openInterestUSDC"]) @@ -1013,7 +1011,7 @@ data class PerpetualMarketSummary( volume24HUSDC = volume24HUSDC, openInterestUSDC = openInterestUSDC, trades24H = trades24H, - markets = newMarkets, + markets = newMarkets.toIMap(), ) } } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/assets/AssetProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/assets/AssetProcessor.kt index 9d46ff36c..602b5dccc 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/assets/AssetProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/assets/AssetProcessor.kt @@ -7,17 +7,16 @@ import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.utils.mutable import exchange.dydx.abacus.utils.safeSet -import indexer.models.configs.AssetJson +import indexer.models.configs.ConfigsMarketAsset internal interface AssetProcessorProtocol { fun process( assetId: String, - payload: AssetJson, + payload: ConfigsMarketAsset, deploymentUri: String, ): Asset } -@Suppress("UNCHECKED_CAST") internal class AssetProcessor( parser: ParserProtocol, private val localizer: LocalizerProtocol? @@ -41,7 +40,7 @@ internal class AssetProcessor( override fun process( assetId: String, - payload: AssetJson, + payload: ConfigsMarketAsset, deploymentUri: String, ): Asset { val imageUrl = "$deploymentUri/currencies/${assetId.lowercase()}.png" diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/assets/AssetsProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/assets/AssetsProcessor.kt index 1938b0d4e..bc677884e 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/assets/AssetsProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/assets/AssetsProcessor.kt @@ -6,7 +6,7 @@ import exchange.dydx.abacus.processor.utils.MarketId import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.utils.mutable -import indexer.models.configs.AssetJson +import indexer.models.configs.ConfigsMarketAsset internal class AssetsProcessor( parser: ParserProtocol, @@ -20,7 +20,7 @@ internal class AssetsProcessor( internal fun processConfigurations( existing: MutableMap, - payload: Map, + payload: Map, deploymentUri: String ): MutableMap { for ((key, data) in payload) { diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketProcessor.kt index 8c83208d8..5ad434782 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketProcessor.kt @@ -7,6 +7,7 @@ import exchange.dydx.abacus.output.MarketStatus import exchange.dydx.abacus.output.PerpetualMarket import exchange.dydx.abacus.output.PerpetualMarketType import exchange.dydx.abacus.processor.base.BaseProcessor +import exchange.dydx.abacus.processor.base.BaseProcessorProtocol import exchange.dydx.abacus.processor.utils.MarketId import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.utils.IndexerResponseParsingException @@ -25,11 +26,17 @@ import kotlin.math.max import kotlin.math.min import kotlin.time.Duration.Companion.seconds +internal interface MarketProcessorProtocol : BaseProcessorProtocol { + fun process(marketId: String, payload: IndexerCompositeMarketObject): PerpetualMarket? + fun processOraclePrice(marketId: String, payload: IndexerWsMarketOraclePriceObject): PerpetualMarket? + fun clearCachedOraclePrice(marketId: String) +} + @Suppress("UNCHECKED_CAST") internal class MarketProcessor( parser: ParserProtocol, private val calculateSparklines: Boolean, -) : BaseProcessor(parser) { +) : BaseProcessor(parser), MarketProcessorProtocol { private val tradesProcessor = TradesProcessor(parser) private val orderbookProcessor = OrderbookProcessor(parser) private val candlesProcessor = CandlesProcessor(parser) @@ -141,28 +148,41 @@ internal class MarketProcessor( ), ) - private var cachedIndexerMarketResponse: IndexerCompositeMarketObject? = null - private var cachedIndexerOraclePrice: IndexerWsMarketOraclePriceObject? = null + private var cachedIndexerMarketResponses: MutableMap = mutableMapOf() + private var cachedIndexerOraclePrices: MutableMap = mutableMapOf() - fun process( + override fun process( + marketId: String, payload: IndexerCompositeMarketObject, ): PerpetualMarket? { - cachedIndexerMarketResponse = if (cachedIndexerMarketResponse != null) { - cachedIndexerMarketResponse?.copyNotNulls(payload) + val cached = cachedIndexerMarketResponses[marketId] + if (cached != null) { + cachedIndexerMarketResponses[marketId] = cached.copyNotNulls(payload) } else { - payload + cachedIndexerMarketResponses[marketId] = payload } - return createPerpetualMarket() + return createPerpetualMarket(marketId) } - fun processOraclePrice( + override fun processOraclePrice( + marketId: String, payload: IndexerWsMarketOraclePriceObject ): PerpetualMarket? { - cachedIndexerOraclePrice = payload - return createPerpetualMarket() + cachedIndexerOraclePrices[marketId] = payload + return createPerpetualMarket(marketId) } - private fun createPerpetualMarket(): PerpetualMarket? { + override fun clearCachedOraclePrice( + marketId: String, + ) { + cachedIndexerOraclePrices.remove(marketId) + } + + private fun createPerpetualMarket( + marketId: String, + ): PerpetualMarket? { + val cachedIndexerMarketResponse = cachedIndexerMarketResponses[marketId] + val cachedIndexerOraclePrice = cachedIndexerOraclePrices[marketId] val payload = cachedIndexerMarketResponse ?: return null val name = parser.asString(payload.ticker) ?: return null val oraclePrice = parser.asDouble(cachedIndexerOraclePrice?.oraclePrice) ?: parser.asDouble(payload.oraclePrice) @@ -184,7 +204,7 @@ internal class MarketProcessor( ), status = status, configs = createConfigs(payload), - perpetual = createMarketPerpetual(payload), + perpetual = createMarketPerpetual(payload, oraclePrice), ) return newValue } catch (e: IndexerResponseParsingException) { @@ -239,7 +259,7 @@ internal class MarketProcessor( minOrderSize = stepSize, initialMarginFraction = parser.asDouble(payload.initialMarginFraction), maintenanceMarginFraction = parser.asDouble(payload.maintenanceMarginFraction), - incrementalInitialMarginFraction = null, + incrementalInitialMarginFraction = parser.asDouble(payload.incrementalInitialMarginFraction), incrementalPositionSize = parser.asDouble(payload.incrementalPositionSize), maxPositionSize = parser.asDouble(payload.maxPositionSize), basePositionNotional = null, @@ -277,10 +297,10 @@ internal class MarketProcessor( private fun createMarketPerpetual( payload: IndexerCompositeMarketObject, + oraclePrice: Double? = null, ): MarketPerpetual? { val nextFundingRate = parser.asDouble(payload.nextFundingRate) val openInterest = parser.asDouble(payload.openInterest) - val oraclePrice = parser.asDouble(payload.oraclePrice) return if (openInterest != null) { MarketPerpetual( volume24H = parser.asDouble(payload.volume24H), @@ -391,7 +411,7 @@ internal class MarketProcessor( } } - internal fun receivedConfigurations( + internal fun receivedConfigurationsDeprecated( market: Map?, payload: Map, ): Map { diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsProcessor.kt index 6047be0cb..ae274b210 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsProcessor.kt @@ -1,6 +1,7 @@ package exchange.dydx.abacus.processor.markets import exchange.dydx.abacus.processor.base.BaseProcessor +import exchange.dydx.abacus.processor.base.BaseProcessorProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.internalstate.InternalMarketState import exchange.dydx.abacus.state.internalstate.InternalMarketSummaryState @@ -10,24 +11,44 @@ import exchange.dydx.abacus.utils.safeSet import indexer.models.IndexerCompositeMarketObject import indexer.models.IndexerWsMarketUpdateResponse +internal interface MarketsProcessorProtocol : BaseProcessorProtocol { + fun processSubscribed( + existing: InternalMarketSummaryState, + content: Map? + ): InternalMarketSummaryState + + fun processChannelData( + existing: InternalMarketSummaryState, + content: IndexerWsMarketUpdateResponse?, + ): InternalMarketSummaryState + + fun processChannelBatchData( + existing: InternalMarketSummaryState, + content: List?, + ): InternalMarketSummaryState +} + internal class MarketsProcessor( parser: ParserProtocol, - calculateSparklines: Boolean -) : BaseProcessor(parser) { - private val marketProcessor = MarketProcessor(parser, calculateSparklines) + calculateSparklines: Boolean, + private val marketProcessor: MarketProcessorProtocol = MarketProcessor(parser, calculateSparklines) +) : BaseProcessor(parser), MarketsProcessorProtocol { + private val marketProcessorDeprecated: MarketProcessor? = marketProcessor as? MarketProcessor internal var groupingMultiplier: Int - get() = marketProcessor.groupingMultiplier + get() = marketProcessorDeprecated!!.groupingMultiplier set(value) { - marketProcessor.groupingMultiplier = value + marketProcessorDeprecated?.groupingMultiplier = value } - fun processSubscribed( + override fun processSubscribed( existing: InternalMarketSummaryState, content: Map? ): InternalMarketSummaryState { for ((marketId, marketData) in content ?: mapOf()) { + marketProcessor.clearCachedOraclePrice(marketId) // Clear cached oracle price if it's new subscription val receivedMarket = marketProcessor.process( + marketId = marketId, payload = marketData, ) val marketState = existing.markets[marketId] ?: InternalMarketState() @@ -37,7 +58,7 @@ internal class MarketsProcessor( return existing } - fun processChannelData( + override fun processChannelData( existing: InternalMarketSummaryState, content: IndexerWsMarketUpdateResponse?, ): InternalMarketSummaryState { @@ -46,6 +67,7 @@ internal class MarketsProcessor( for ((marketId, marketData) in content.trading) { val marketState = existing.markets[marketId] ?: InternalMarketState() val receivedMarket = marketProcessor.process( + marketId = marketId, payload = marketData, ) if (receivedMarket != marketState.perpetualMarket) { @@ -58,6 +80,7 @@ internal class MarketsProcessor( for ((marketId, oracleData) in content.oraclePrices) { val marketState = existing.markets[marketId] ?: InternalMarketState() val receivedMarket = marketProcessor.processOraclePrice( + marketId = marketId, payload = oracleData, ) if (receivedMarket != marketState.perpetualMarket) { @@ -70,7 +93,7 @@ internal class MarketsProcessor( return existing } - fun processChannelBatchData( + override fun processChannelBatchData( existing: InternalMarketSummaryState, content: List?, ): InternalMarketSummaryState { @@ -92,7 +115,7 @@ internal class MarketsProcessor( } } - internal fun processChannelData( + internal fun processChannelDataDeprecated( existing: Map?, content: Map ): Map { @@ -120,7 +143,7 @@ internal class MarketsProcessor( for ((market, data) in payload) { val marketPayload = parser.asNativeMap(data) if (marketPayload != null) { - val receivedMarket = marketProcessor.received( + val receivedMarket = marketProcessorDeprecated!!.received( parser.asNativeMap(existing?.get(market)), marketPayload, ) @@ -139,7 +162,7 @@ internal class MarketsProcessor( for ((market, data) in narrowedPayload) { val marketPayload = parser.asNativeMap(data) if (marketPayload != null) { - val receivedMarket = marketProcessor.receivedDeltaDeprecated( + val receivedMarket = marketProcessorDeprecated!!.receivedDeltaDeprecated( parser.asNativeMap(existing?.get(market)), marketPayload, ) @@ -154,7 +177,7 @@ internal class MarketsProcessor( ?: parser.asNativeMap("markets") ?: payload } - internal fun receivedConfigurations( + internal fun receivedConfigurationsDeprecated( existing: Map?, payload: Map ): Map { @@ -164,7 +187,7 @@ internal class MarketsProcessor( if (marketPayload == null) { Logger.d { "Market payload is null" } } else { - val receivedMarket = marketProcessor.receivedConfigurations( + val receivedMarket = marketProcessorDeprecated!!.receivedConfigurationsDeprecated( parser.asNativeMap(existing?.get(market)), marketPayload, ) @@ -181,7 +204,7 @@ internal class MarketsProcessor( ): Map { val marketData = parser.asNativeMap(existing?.get(market)) ?: mutableMapOf() val markets = existing?.mutable() ?: mutableMapOf() - markets[market] = marketProcessor.receivedOrderbook(marketData, payload) + markets[market] = marketProcessorDeprecated!!.receivedOrderbook(marketData, payload) return markets } @@ -193,7 +216,7 @@ internal class MarketsProcessor( val marketData = parser.asNativeMap(existing?.get(market)) return if (existing != null && marketData != null) { val markets = existing.mutable() - markets[market] = marketProcessor.receivedBatchOrderbookChanges(marketData, payload) + markets[market] = marketProcessorDeprecated!!.receivedBatchOrderbookChanges(marketData, payload) markets } else { existing @@ -208,7 +231,7 @@ internal class MarketsProcessor( ): Map { val marketData = parser.asNativeMap(existing?.get(market)) ?: mutableMapOf() val markets = existing?.mutable() ?: mutableMapOf() - markets[market] = marketProcessor.receivedTradesDeprecated(marketData, payload) + markets[market] = marketProcessorDeprecated!!.receivedTradesDeprecated(marketData, payload) return markets } @@ -221,7 +244,7 @@ internal class MarketsProcessor( val marketData = parser.asNativeMap(existing?.get(market)) return if (existing != null && marketData != null) { val markets = existing.mutable() - markets[market] = marketProcessor.receivedTradesChangesDeprecated(marketData, payload) + markets[market] = marketProcessorDeprecated!!.receivedTradesChangesDeprecated(marketData, payload) markets } else { existing @@ -236,7 +259,7 @@ internal class MarketsProcessor( val marketData = parser.asNativeMap(existing?.get(market)) return if (existing != null && marketData != null) { val markets = existing.mutable() - markets[market] = marketProcessor.receivedBatchedTradesChanges(marketData, payload) + markets[market] = marketProcessorDeprecated!!.receivedBatchedTradesChanges(marketData, payload) markets } else { existing @@ -254,7 +277,7 @@ internal class MarketsProcessor( parser.asString(key)?.let { market -> parser.asNativeMap(existing?.get(market))?.let { marketData -> parser.asNativeList(itemData)?.let { list -> - modified[market] = marketProcessor.receivedCandles(marketData, list) + modified[market] = marketProcessorDeprecated!!.receivedCandles(marketData, list) } } } @@ -269,7 +292,7 @@ internal class MarketsProcessor( val modified = existing?.mutable() ?: mutableMapOf() parser.asNativeMap(existing?.get(market))?.let { marketData -> - modified[market] = marketProcessor.receivedCandles(marketData, list) + modified[market] = marketProcessorDeprecated!!.receivedCandles(marketData, list) } return modified } @@ -288,7 +311,7 @@ internal class MarketsProcessor( parser.asString(key)?.let { market -> parser.asNativeMap(existing?.get(market))?.let { marketData -> parser.asNativeList(itemData)?.let { list -> - modified[market] = marketProcessor.receivedSparklines(marketData, list) + modified[market] = marketProcessorDeprecated!!.receivedSparklines(marketData, list) } } } @@ -305,7 +328,7 @@ internal class MarketsProcessor( val marketData = parser.asNativeMap(existing?.get(market)) return if (existing != null && marketData != null) { val markets = existing.mutable() - markets[market] = marketProcessor.receivedCandles(marketData, resolution, payload) + markets[market] = marketProcessorDeprecated!!.receivedCandles(marketData, resolution, payload) markets } else { existing @@ -322,7 +345,7 @@ internal class MarketsProcessor( return if (existing != null && marketData != null) { val markets = existing.mutable() markets[market] = - marketProcessor.receivedCandlesChanges(marketData, resolution, payload) + marketProcessorDeprecated!!.receivedCandlesChanges(marketData, resolution, payload) markets } else { existing @@ -339,7 +362,7 @@ internal class MarketsProcessor( return if (existing != null && marketData != null) { val markets = existing.mutable() markets[market] = - marketProcessor.receivedBatchedCandlesChanges(marketData, resolution, payload) + marketProcessorDeprecated!!.receivedBatchedCandlesChanges(marketData, resolution, payload) markets } else { existing @@ -360,7 +383,7 @@ internal class MarketsProcessor( val marketData = parser.asNativeMap(existing?.get(market)) if (existing != null && marketData != null) { val markets = existing.mutable() - markets[market] = marketProcessor.receivedHistoricalFundings(marketData, payload) + markets[market] = marketProcessorDeprecated!!.receivedHistoricalFundings(marketData, payload) return markets } } @@ -372,11 +395,11 @@ internal class MarketsProcessor( val modified = existing.mutable() if (market != null) { val existingMarket = parser.asNativeMap(existing[market]) - modified.safeSet(market, marketProcessor.groupOrderbook(existingMarket)) + modified.safeSet(market, marketProcessorDeprecated!!.groupOrderbook(existingMarket)) } else { for ((key, value) in existing) { val existingMarket = parser.asNativeMap(value) - modified.safeSet(key, marketProcessor.groupOrderbook(existingMarket)) + modified.safeSet(key, marketProcessorDeprecated!!.groupOrderbook(existingMarket)) } } modified diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsSummaryProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsSummaryProcessor.kt index b66a40d16..bf57df9cd 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsSummaryProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsSummaryProcessor.kt @@ -57,7 +57,7 @@ internal class MarketsSummaryProcessor( existing: Map?, content: Map ): Map? { - val markets = marketsProcessor.processChannelData(parser.asNativeMap(existing?.get("markets")), content) + val markets = marketsProcessor.processChannelDataDeprecated(parser.asNativeMap(existing?.get("markets")), content) return modify(existing, markets) } @@ -71,12 +71,12 @@ internal class MarketsSummaryProcessor( return modify(existing, markets) } - internal fun receivedConfigurations( + internal fun receivedConfigurationsDeprecated( existing: Map?, payload: Map ): Map { val markets = - marketsProcessor.receivedConfigurations(parser.asNativeMap(existing?.get("markets")), payload) + marketsProcessor.receivedConfigurationsDeprecated(parser.asNativeMap(existing?.get("markets")), payload) return modify(existing, markets)!! } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt index e801fe60b..04716b842 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt @@ -157,9 +157,9 @@ internal data class InternalPerpetualPosition( get() { return if (subaccountNumber != null) { if (subaccountNumber >= NUM_PARENT_SUBACCOUNTS) { - MarginMode.Cross - } else { MarginMode.Isolated + } else { + MarginMode.Cross } } else { null diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Markets.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Markets.kt index 208fb1490..839ca8dba 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Markets.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Markets.kt @@ -8,7 +8,7 @@ import exchange.dydx.abacus.state.changes.Changes import exchange.dydx.abacus.state.changes.StateChanges import indexer.models.IndexerCompositeMarketObject import indexer.models.IndexerWsMarketUpdateResponse -import indexer.models.configs.AssetJson +import indexer.models.configs.ConfigsMarketAsset import kollections.iListOf import kollections.toIList @@ -134,15 +134,10 @@ internal fun TradingStateMachine.receivedBatchedMarketsChanges( } internal fun TradingStateMachine.processMarketsConfigurations( - payload: Map, + payload: Map, subaccountNumber: Int?, deploymentUri: String, ): StateChanges { - this.marketsSummary = marketsProcessor.receivedConfigurations( - existing = this.marketsSummary, - payload = payload, - ) - internalState.assets = assetsProcessor.processConfigurations( existing = internalState.assets, payload = payload, @@ -181,7 +176,7 @@ internal fun TradingStateMachine.receivedMarketsConfigurationsDeprecated( subaccountNumber: Int?, deploymentUri: String, ): StateChanges { - this.marketsSummary = marketsProcessor.receivedConfigurations( + this.marketsSummary = marketsProcessor.receivedConfigurationsDeprecated( existing = this.marketsSummary, payload = payload, ) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt index e37fbbf73..0e1f54ca5 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt @@ -71,7 +71,7 @@ import exchange.dydx.abacus.utils.mutableMapOf import exchange.dydx.abacus.utils.safeSet import exchange.dydx.abacus.utils.typedSafeSet import exchange.dydx.abacus.validator.InputValidator -import indexer.models.configs.AssetJson +import indexer.models.configs.ConfigsMarketAsset import kollections.JsExport import kollections.iListOf import kollections.iMutableListOf @@ -591,7 +591,7 @@ open class TradingStateMachine( ): StateChanges { val json = parser.decodeJsonObject(payload) if (staticTyping) { - val parsedAssetPayload = parser.asTypedStringMap(json) + val parsedAssetPayload = parser.asTypedStringMap(json) if (parsedAssetPayload == null) { Logger.e { "Error parsing asset payload" } return StateChanges.noChange diff --git a/src/commonMain/kotlin/indexer/models/IndexerCompositeMarketObject.kt b/src/commonMain/kotlin/indexer/models/IndexerCompositeMarketObject.kt index b0175ae94..732b23e5c 100644 --- a/src/commonMain/kotlin/indexer/models/IndexerCompositeMarketObject.kt +++ b/src/commonMain/kotlin/indexer/models/IndexerCompositeMarketObject.kt @@ -39,6 +39,9 @@ data class IndexerCompositeMarketObject( val basePositionSize: kotlin.String? = null, val incrementalPositionSize: kotlin.String? = null, val maxPositionSize: kotlin.String? = null, + + // Unused fields + val incrementalInitialMarginFraction: kotlin.String? = null, ) { fun copyNotNulls(from: IndexerCompositeMarketObject): IndexerCompositeMarketObject { return IndexerCompositeMarketObject( @@ -70,6 +73,7 @@ data class IndexerCompositeMarketObject( basePositionSize = from.basePositionSize ?: basePositionSize, incrementalPositionSize = from.incrementalPositionSize ?: incrementalPositionSize, maxPositionSize = from.maxPositionSize ?: maxPositionSize, + incrementalInitialMarginFraction = from.incrementalInitialMarginFraction ?: incrementalInitialMarginFraction, ) } } diff --git a/src/commonMain/kotlin/indexer/models/configs/AssetJson.kt b/src/commonMain/kotlin/indexer/models/configs/ConfigsMarketAssetResponse.kt similarity index 92% rename from src/commonMain/kotlin/indexer/models/configs/AssetJson.kt rename to src/commonMain/kotlin/indexer/models/configs/ConfigsMarketAssetResponse.kt index d4f8c99b5..1c480ea34 100644 --- a/src/commonMain/kotlin/indexer/models/configs/AssetJson.kt +++ b/src/commonMain/kotlin/indexer/models/configs/ConfigsMarketAssetResponse.kt @@ -7,7 +7,7 @@ import kotlinx.serialization.Serializable * AssetJson from ${V4_WEB_URL}/configs/markets.json */ @Serializable -data class AssetJson( +data class ConfigsMarketAsset( val name: String, val websiteLink: String? = null, val whitepaperLink: String? = null, diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/payload/BaseTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/payload/BaseTests.kt index 9f88b7c24..9874e7bd0 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/payload/BaseTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/payload/BaseTests.kt @@ -273,12 +273,18 @@ open class BaseTests( } else { verifyAssetsState(perp.assets, state?.assets, "assets") } - verifyMarketsState( - perp.marketsSummary, - perp.assets, - state?.marketsSummary, - "markets", - ) + if (staticTyping) { + for ((key, value) in perp.internalState.marketsSummary.markets) { + assertEquals(value.perpetualMarket, state?.marketsSummary?.markets?.get(key)) + } + } else { + verifyMarketsState( + perp.marketsSummary, + perp.assets, + state?.marketsSummary, + "markets", + ) + } verifyMarketsHistoricalFundingsState( parser.asNativeMap(perp.marketsSummary?.get("markets")), state?.historicalFundings, diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/processor/markets/MarketProcessorTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/processor/markets/MarketProcessorTests.kt new file mode 100644 index 000000000..94e777e1d --- /dev/null +++ b/src/commonTest/kotlin/exchange.dydx.abacus/processor/markets/MarketProcessorTests.kt @@ -0,0 +1,146 @@ +package exchange.dydx.abacus.processor.markets + +import exchange.dydx.abacus.output.MarketConfigs +import exchange.dydx.abacus.output.MarketConfigsV4 +import exchange.dydx.abacus.output.MarketPerpetual +import exchange.dydx.abacus.output.MarketStatus +import exchange.dydx.abacus.output.PerpetualMarket +import exchange.dydx.abacus.output.PerpetualMarketType +import exchange.dydx.abacus.utils.Parser +import indexer.codegen.IndexerPerpetualMarketStatus +import indexer.codegen.IndexerPerpetualMarketType +import indexer.models.IndexerCompositeMarketObject +import indexer.models.IndexerWsMarketOraclePriceObject +import kotlin.test.Test +import kotlin.test.assertEquals + +class MarketProcessorTests { + companion object { + val marketPayloadMock = IndexerCompositeMarketObject( + clobPairId = "1", + ticker = "BTC-USD", + status = IndexerPerpetualMarketStatus.ACTIVE, + oraclePrice = "20000.0000", + priceChange24H = "138.180620", + volume24H = "424522782.317100", + trades24H = 54584, + nextFundingRate = "0.0000102743", + initialMarginFraction = "0.05", + maintenanceMarginFraction = "0.03", + openInterest = "5061.9983", + atomicResolution = 10000, + quantumConversionExponent = 10, + tickSize = "0.1", + stepSize = "0.0001", + stepBaseQuantums = 10000, + subticksPerTick = 1, + marketType = IndexerPerpetualMarketType.CROSS, + openInterestLowerCap = null, + openInterestUpperCap = null, + baseOpenInterest = null, + id = null, + marketId = null, + baseAsset = null, + quoteAsset = null, + basePositionSize = null, + incrementalPositionSize = null, + maxPositionSize = null, + incrementalInitialMarginFraction = null, + ) + + val marketUpdatePayloadMock = IndexerCompositeMarketObject( + incrementalPositionSize = "5", + basePositionSize = "25", + ) + + val oraclePricePayloadMock = IndexerWsMarketOraclePriceObject( + oraclePrice = "10000.0000", + ) + + val outputMock = PerpetualMarket( + id = "BTC-USD", + assetId = "BTC", + market = "BTC-USD", + oraclePrice = 20000.0000, + marketCaps = null, + priceChange24H = 138.180620, + priceChange24HPercent = 0.006957097804400636, + status = MarketStatus( + canTrade = true, + canReduce = true, + ), + configs = MarketConfigs( + initialMarginFraction = 0.05, + maintenanceMarginFraction = 0.03, + tickSize = 0.1, + stepSize = 0.0001, + incrementalPositionSize = null, + maxPositionSize = null, + incrementalInitialMarginFraction = null, + clobPairId = "1", + largeSize = null, + stepSizeDecimals = 4, + tickSizeDecimals = 1, + displayStepSize = 0.0001, + displayTickSize = 0.1, + displayStepSizeDecimals = 4, + displayTickSizeDecimals = 1, + effectiveInitialMarginFraction = 0.05, + minOrderSize = 0.0001, + basePositionNotional = null, + baselinePositionSize = null, + candleOptions = null, + perpetualMarketType = PerpetualMarketType.CROSS, + v4 = MarketConfigsV4( + atomicResolution = 10000, + quantumConversionExponent = 10, + subticksPerTick = 1, + clobPairId = 1, + stepBaseQuantums = 10000, + ), + ), + perpetual = MarketPerpetual( + volume24H = 424522782.317100, + trades24H = 54584.0, + openInterest = 5061.9983, + nextFundingRate = 0.0000102743, + openInterestLowerCap = null, + openInterestUpperCap = null, + volume24HUSDC = null, + nextFundingAtMilliseconds = null, + openInterestUSDC = 101239966.0, + line = null, + ), + ) + } + + private val processor = MarketProcessor( + parser = Parser(), + calculateSparklines = true, + ) + + @Test + fun testProcess() { + var output = processor.process("BTC-USD", marketPayloadMock) + assertEquals(outputMock, output) + + output = processor.process("BTC-USD", marketUpdatePayloadMock) + assertEquals( + outputMock.copy( + configs = outputMock.configs?.copy( + incrementalPositionSize = 5.0, + baselinePositionSize = 25.0, + ), + ), + output, + ) + } + + @Test + fun testProcessOraclePrice() { + processor.process("BTC-USD", marketPayloadMock) + val output = processor.processOraclePrice("BTC-USD", oraclePricePayloadMock) + assertEquals(output?.oraclePrice, 10000.0000) + assertEquals(output?.priceChange24HPercent, 0.014011676210602023) + } +} diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/processor/markets/MarketsProcessorTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/processor/markets/MarketsProcessorTests.kt new file mode 100644 index 000000000..3abb66a22 --- /dev/null +++ b/src/commonTest/kotlin/exchange.dydx.abacus/processor/markets/MarketsProcessorTests.kt @@ -0,0 +1,85 @@ +package exchange.dydx.abacus.processor.markets + +import exchange.dydx.abacus.processor.wallet.account.HistoricalPNLProcessorTests.Companion.payload +import exchange.dydx.abacus.state.internalstate.InternalMarketSummaryState +import exchange.dydx.abacus.tests.mock.processor.markets.MarketProcessorMock +import exchange.dydx.abacus.utils.Parser +import indexer.models.IndexerWsMarketUpdateResponse +import kotlin.test.Test +import kotlin.test.assertEquals + +class MarketsProcessorTests { + private val marketProcessor = MarketProcessorMock() + private val processor = MarketsProcessor( + parser = Parser(), + marketProcessor = marketProcessor, + calculateSparklines = false, + ) + + @Test + fun testProcessSubscribed() { + val state = InternalMarketSummaryState() + marketProcessor.processAction = { _, _ -> + MarketProcessorTests.outputMock + } + + val payload = mapOf( + "BTC-USD" to MarketProcessorTests.marketPayloadMock, + "ETH-USD" to MarketProcessorTests.marketPayloadMock, + ) + val result = processor.processSubscribed(state, payload) + assertEquals(2, result.markets.size) + } + + @Test + fun testProcessChannelData() { + val state = InternalMarketSummaryState() + marketProcessor.processAction = { _, _ -> + MarketProcessorTests.outputMock + } + + val payload = IndexerWsMarketUpdateResponse( + trading = mapOf( + "BTC-USD" to MarketProcessorTests.marketPayloadMock, + "ETH-USD" to MarketProcessorTests.marketPayloadMock, + ), + ) + val result = processor.processChannelData(state, payload) + assertEquals(2, result.markets.size) + + val oraclePricePayload = IndexerWsMarketUpdateResponse( + oraclePrices = mapOf( + "BTC-USD" to MarketProcessorTests.oraclePricePayloadMock, + "ETH-USD" to MarketProcessorTests.oraclePricePayloadMock, + ), + ) + marketProcessor.processOraclePriceAction = { _, _ -> + MarketProcessorTests.outputMock + } + val updatedResult = processor.processChannelData(state, oraclePricePayload) + assertEquals(2, updatedResult.markets.size) + assertEquals(20000.0, updatedResult.markets["BTC-USD"]?.perpetualMarket?.oraclePrice) + } + + @Test + fun testProcessBatchChannelData() { + val state = InternalMarketSummaryState() + val payload = listOf( + IndexerWsMarketUpdateResponse( + trading = mapOf( + "BTC-USD" to MarketProcessorTests.marketPayloadMock, + "ETH-USD" to MarketProcessorTests.marketPayloadMock, + ), + ), + IndexerWsMarketUpdateResponse( + oraclePrices = mapOf( + "BTC-USD" to MarketProcessorTests.oraclePricePayloadMock, + "ETH-USD" to MarketProcessorTests.oraclePricePayloadMock, + ), + ), + ) + val result = processor.processChannelBatchData(state, payload) + assertEquals(2, marketProcessor.processCallCount) + assertEquals(2, marketProcessor.processOraclePriceCallCount) + } +} diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/tests/mock/processor/markets/MarketProcessorMock.kt b/src/commonTest/kotlin/exchange.dydx.abacus/tests/mock/processor/markets/MarketProcessorMock.kt new file mode 100644 index 000000000..68c992241 --- /dev/null +++ b/src/commonTest/kotlin/exchange.dydx.abacus/tests/mock/processor/markets/MarketProcessorMock.kt @@ -0,0 +1,40 @@ +package exchange.dydx.abacus.tests.mock.processor.markets + +import exchange.dydx.abacus.output.PerpetualMarket +import exchange.dydx.abacus.processor.markets.MarketProcessorProtocol +import exchange.dydx.abacus.state.manager.V4Environment +import indexer.models.IndexerCompositeMarketObject +import indexer.models.IndexerWsMarketOraclePriceObject + +class MarketProcessorMock : MarketProcessorProtocol { + var processAction: ((String, IndexerCompositeMarketObject) -> PerpetualMarket?)? = null + var processOraclePriceAction: ((String, IndexerWsMarketOraclePriceObject) -> PerpetualMarket?)? = null + var clearCachedOraclePriceAction: ((String) -> Unit)? = null + var processCallCount = 0 + var processOraclePriceCallCount = 0 + var clearCachedOraclePriceCallCount = 0 + + override fun process( + marketId: String, + payload: IndexerCompositeMarketObject + ): PerpetualMarket? { + processCallCount++ + return processAction?.invoke(marketId, payload) + } + + override fun processOraclePrice( + marketId: String, + payload: IndexerWsMarketOraclePriceObject + ): PerpetualMarket? { + processOraclePriceCallCount++ + return processOraclePriceAction?.invoke(marketId, payload) + } + + override fun clearCachedOraclePrice(marketId: String) { + clearCachedOraclePriceCallCount++ + clearCachedOraclePriceAction?.invoke(marketId) + } + + override var accountAddress: String? = null + override var environment: V4Environment? = null +} diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/tests/payloads/MarketsChannelMock.kt b/src/commonTest/kotlin/exchange.dydx.abacus/tests/payloads/MarketsChannelMock.kt index 400937b12..be02b9ae2 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/tests/payloads/MarketsChannelMock.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/tests/payloads/MarketsChannelMock.kt @@ -2757,15 +2757,17 @@ internal class MarketsChannelMock { "message_id":2, "channel":"v4_markets", "contents":{ - "BTC-USD":{ - "volume24H":"493681565.92757831256", - "trades24H":922900, - "openInterest":"3531.250439547" - }, - "ETH-USD":{ - "volume24H":"493203231.416110155", - "trades24H":939491, - "openInterest":"46115.767606" + "trading": { + "BTC-USD":{ + "volume24H":"493681565.92757831256", + "trades24H":922900, + "openInterest":"3531.250439547" + }, + "ETH-USD":{ + "volume24H":"493203231.416110155", + "trades24H":939491, + "openInterest":"46115.767606" + } } } } @@ -2806,7 +2808,7 @@ internal class MarketsChannelMock { { "oraclePrices":{ "BTC-USD":{ - "price": "21000.00", + "oraclePrice": "21000.00", "effectiveAt": "2022-06-01T12:01:01.002Z", "effectiveAtHeight": "500", "marketId": 0 From 73868c0f9c44510646876f1b9b677cd82a2f431f Mon Sep 17 00:00:00 2001 From: mobile-build-bot-git Date: Fri, 2 Aug 2024 18:37:28 +0000 Subject: [PATCH 03/63] Bump version --- build.gradle.kts | 2 +- v4_abacus.podspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 9899c055d..71ee6457e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -52,7 +52,7 @@ allprojects { } group = "exchange.dydx.abacus" -version = "1.8.76" +version = "1.8.77" repositories { google() diff --git a/v4_abacus.podspec b/v4_abacus.podspec index 1635ba18f..d61c9d1fd 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.76' + spec.version = '1.8.77' spec.homepage = 'https://github.com/dydxprotocol/v4-abacus' spec.source = { :http=> ''} spec.authors = '' From 5b30eef8082723b4ae4833a661426eb2d222f30a Mon Sep 17 00:00:00 2001 From: Rui Date: Fri, 2 Aug 2024 18:08:15 -0700 Subject: [PATCH 04/63] Sparklines --- .../processor/markets/MarketProcessor.kt | 20 +++++-- .../processor/markets/MarketsProcessor.kt | 27 ++++++++- .../markets/MarketsSummaryProcessor.kt | 12 +++- .../protocols/ParserProtocol.kt | 20 +++++++ .../model/TradingStateMachine+Candles.kt | 25 +++++--- .../payload/v4/V4MarketsTests.kt | 60 +++++++++++++------ .../processor/markets/MarketProcessorTests.kt | 8 +++ .../markets/MarketsProcessorTests.kt | 23 ++++++- .../processor/markets/MarketProcessorMock.kt | 7 +++ 9 files changed, 166 insertions(+), 36 deletions(-) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketProcessor.kt index 5ad434782..24639a037 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketProcessor.kt @@ -21,6 +21,7 @@ import indexer.codegen.IndexerPerpetualMarketStatus import indexer.codegen.IndexerPerpetualMarketType import indexer.models.IndexerCompositeMarketObject import indexer.models.IndexerWsMarketOraclePriceObject +import kollections.toIList import numberOfDecimals import kotlin.math.max import kotlin.math.min @@ -29,6 +30,7 @@ import kotlin.time.Duration.Companion.seconds internal interface MarketProcessorProtocol : BaseProcessorProtocol { fun process(marketId: String, payload: IndexerCompositeMarketObject): PerpetualMarket? fun processOraclePrice(marketId: String, payload: IndexerWsMarketOraclePriceObject): PerpetualMarket? + fun processSparklines(marketId: String, payload: List): PerpetualMarket? fun clearCachedOraclePrice(marketId: String) } @@ -150,6 +152,7 @@ internal class MarketProcessor( private var cachedIndexerMarketResponses: MutableMap = mutableMapOf() private var cachedIndexerOraclePrices: MutableMap = mutableMapOf() + private var cachedIndexerSparklines: MutableMap> = mutableMapOf() override fun process( marketId: String, @@ -172,6 +175,14 @@ internal class MarketProcessor( return createPerpetualMarket(marketId) } + override fun processSparklines( + marketId: String, + payload: List, + ): PerpetualMarket? { + cachedIndexerSparklines[marketId] = payload.mapNotNull { parser.asDouble(it) }.reversed() + return createPerpetualMarket(marketId) + } + override fun clearCachedOraclePrice( marketId: String, ) { @@ -204,7 +215,7 @@ internal class MarketProcessor( ), status = status, configs = createConfigs(payload), - perpetual = createMarketPerpetual(payload, oraclePrice), + perpetual = createMarketPerpetual(payload, oraclePrice, cachedIndexerSparklines[marketId]), ) return newValue } catch (e: IndexerResponseParsingException) { @@ -297,7 +308,8 @@ internal class MarketProcessor( private fun createMarketPerpetual( payload: IndexerCompositeMarketObject, - oraclePrice: Double? = null, + oraclePrice: Double?, + line: List?, ): MarketPerpetual? { val nextFundingRate = parser.asDouble(payload.nextFundingRate) val openInterest = parser.asDouble(payload.openInterest) @@ -312,7 +324,7 @@ internal class MarketProcessor( openInterestUSDC = oraclePrice?.let { openInterest * it } ?: 0.0, openInterestLowerCap = parser.asDouble(payload.openInterestLowerCap), openInterestUpperCap = parser.asDouble(payload.openInterestUpperCap), - line = null, + line = line?.toIList(), ) } else { null @@ -583,7 +595,7 @@ internal class MarketProcessor( } } - internal fun receivedSparklines( + internal fun receivedSparklinesDeprecated( market: Map, payload: List, ): Map { diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsProcessor.kt index ae274b210..b22b0dd03 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsProcessor.kt @@ -26,6 +26,11 @@ internal interface MarketsProcessorProtocol : BaseProcessorProtocol { existing: InternalMarketSummaryState, content: List?, ): InternalMarketSummaryState + + fun processSparklines( + existing: InternalMarketSummaryState, + content: Map>? + ): InternalMarketSummaryState } internal class MarketsProcessor( @@ -103,6 +108,24 @@ internal class MarketsProcessor( return existing } + override fun processSparklines( + existing: InternalMarketSummaryState, + content: Map>? + ): InternalMarketSummaryState { + for ((marketId, sparklines) in content ?: mapOf()) { + val marketState = existing.markets[marketId] ?: InternalMarketState() + val receivedMarket = marketProcessor.processSparklines( + marketId = marketId, + payload = sparklines, + ) + if (receivedMarket != marketState.perpetualMarket) { + marketState.perpetualMarket = receivedMarket + existing.markets[marketId] = marketState + } + } + return existing + } + internal fun processSubscribedDeprecated( existing: Map?, content: Map @@ -302,7 +325,7 @@ internal class MarketsProcessor( return existing } - internal fun receivedSparklines( + internal fun receivedSparklinesDeprecated( existing: Map?, payload: Map ): Map { @@ -311,7 +334,7 @@ internal class MarketsProcessor( parser.asString(key)?.let { market -> parser.asNativeMap(existing?.get(market))?.let { marketData -> parser.asNativeList(itemData)?.let { list -> - modified[market] = marketProcessorDeprecated!!.receivedSparklines(marketData, list) + modified[market] = marketProcessorDeprecated!!.receivedSparklinesDeprecated(marketData, list) } } } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsSummaryProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsSummaryProcessor.kt index bf57df9cd..7be440593 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsSummaryProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsSummaryProcessor.kt @@ -44,6 +44,14 @@ internal class MarketsSummaryProcessor( return existing } + fun processSparklines( + existing: InternalMarketSummaryState, + content: Map>?, + ): InternalMarketSummaryState { + val markets = marketsProcessor.processSparklines(existing, content) + return existing + } + internal fun subscribedDeprecated( existing: Map?, content: Map @@ -153,12 +161,12 @@ internal class MarketsSummaryProcessor( return modify(existing, markets) } - internal fun receivedSparklines( + internal fun receivedSparklinesDeprecated( existing: Map?, payload: Map ): Map? { val markets = - marketsProcessor.receivedSparklines(parser.asNativeMap(existing?.get("markets")), payload) + marketsProcessor.receivedSparklinesDeprecated(parser.asNativeMap(existing?.get("markets")), payload) return modify(existing, markets) } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/protocols/ParserProtocol.kt b/src/commonMain/kotlin/exchange.dydx.abacus/protocols/ParserProtocol.kt index 4931172ce..4b4299a29 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/protocols/ParserProtocol.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/protocols/ParserProtocol.kt @@ -63,6 +63,9 @@ internal inline fun ParserProtocol.asTypedList(list: Any?): List? } else { val itemString: String? = asString(item) if (itemString != null) { + if (itemString is T) { + return@mapNotNull itemString + } try { jsonCoder.decodeFromString(itemString) } catch (e: SerializationException) { @@ -91,6 +94,9 @@ internal inline fun ParserProtocol.asTypedObject(item: Any?): T? { asString(item) } return if (itemString != null) { + if (itemString is T) { + itemString + } try { jsonCoder.decodeFromString(itemString) } catch (e: SerializationException) { @@ -120,3 +126,17 @@ internal inline fun ParserProtocol.asTypedStringMap(payload: Map ParserProtocol.asTypedStringMapOfList(payload: Map>?): Map>? { + if (payload == null) { + return null + } + val result = mutableMapOf>() + for ((key, value) in payload) { + val typedValue = asTypedList(value) + if (typedValue != null) { + result[key] = typedValue + } + } + return result +} diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Candles.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Candles.kt index a5d2dcd64..40b4bac28 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Candles.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Candles.kt @@ -1,5 +1,6 @@ package exchange.dydx.abacus.state.model +import exchange.dydx.abacus.protocols.asTypedStringMapOfList import exchange.dydx.abacus.state.changes.Changes import exchange.dydx.abacus.state.changes.StateChanges import kollections.iListOf @@ -43,19 +44,25 @@ private fun TradingStateMachine.receivedCandles(payload: Map): Stat } internal fun TradingStateMachine.sparklines(payload: String): StateChanges? { - val json = parser.decodeJsonObject(payload) - return if (json != null) { - receivedSparklines(json) + val json = parser.decodeJsonObject(payload) as? Map> + if (staticTyping) { + val sparklines = parser.asTypedStringMapOfList(json) + return if (sparklines != null) { + marketsProcessor.processSparklines(internalState.marketsSummary, sparklines) + StateChanges(iListOf(Changes.sparklines, Changes.markets), null) + } else { + StateChanges.noChange + } } else { - StateChanges.noChange + return if (json != null) { + marketsSummary = marketsProcessor.receivedSparklinesDeprecated(marketsSummary, json) + return StateChanges(iListOf(Changes.sparklines, Changes.markets), null) + } else { + StateChanges.noChange + } } } -private fun TradingStateMachine.receivedSparklines(payload: Map): StateChanges { - marketsSummary = marketsProcessor.receivedSparklines(marketsSummary, payload) - return StateChanges(iListOf(Changes.sparklines, Changes.markets), null) -} - internal fun TradingStateMachine.receivedCandles( market: String, resolution: String, diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4MarketsTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4MarketsTests.kt index ab9a36b81..389430f0f 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4MarketsTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4MarketsTests.kt @@ -197,16 +197,27 @@ class V4MarketsTests : V4BaseTests() { } private fun testMarketsSparklinesChanged() { - test( - { - perp.rest( - AbUrl.fromString("$testRestUrl/v4/sparklines?timePeriod=ONE_DAY"), - mock.candles.v4SparklinesFirstCall, - 0, - null, - ) - }, - """ + if (perp.staticTyping) { + perp.rest( + url = AbUrl.fromString("$testRestUrl/v4/sparklines?timePeriod=ONE_DAY"), + payload = mock.candles.v4SparklinesFirstCall, + subaccountNumber = 0, + height = null, + ) + val btcLine = perp.internalState.marketsSummary.markets["BTC-USD"]?.perpetualMarket?.perpetual?.line + assertEquals(btcLine?.get(0), 29308.0) + assertEquals(btcLine?.get(1), 29373.0) + } else { + test( + { + perp.rest( + AbUrl.fromString("$testRestUrl/v4/sparklines?timePeriod=ONE_DAY"), + mock.candles.v4SparklinesFirstCall, + 0, + null, + ) + }, + """ { "markets": { "markets": { @@ -230,15 +241,27 @@ class V4MarketsTests : V4BaseTests() { } } """, - ) + ) + } } private fun testMarketsChanged() { - test( - { - perp.loadv4MarketsChanged(mock, testWsUrl) - }, - """ + if (perp.staticTyping) { + perp.loadv4MarketsChanged(mock, testWsUrl) + val btcPerpetual = perp.internalState.marketsSummary.markets["BTC-USD"]?.perpetualMarket?.perpetual + assertEquals(btcPerpetual?.openInterest, 3531.250439547) + assertEquals(btcPerpetual?.volume24H, 493681565.92757831256) + assertEquals(btcPerpetual?.trades24H, 922900.0) + val ethPerpetual = perp.internalState.marketsSummary.markets["ETH-USD"]?.perpetualMarket?.perpetual + assertEquals(ethPerpetual?.openInterest, 46115.767606) + assertEquals(ethPerpetual?.volume24H, 493203231.416110155) + assertEquals(ethPerpetual?.trades24H, 939491.0) + } else { + test( + { + perp.loadv4MarketsChanged(mock, testWsUrl) + }, + """ { "markets":{ "markets":{ @@ -299,8 +322,9 @@ class V4MarketsTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } private fun testMarketsBatchChanged() { diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/processor/markets/MarketProcessorTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/processor/markets/MarketProcessorTests.kt index 94e777e1d..f64407a05 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/processor/markets/MarketProcessorTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/processor/markets/MarketProcessorTests.kt @@ -11,6 +11,7 @@ import indexer.codegen.IndexerPerpetualMarketStatus import indexer.codegen.IndexerPerpetualMarketType import indexer.models.IndexerCompositeMarketObject import indexer.models.IndexerWsMarketOraclePriceObject +import kollections.toIList import kotlin.test.Test import kotlin.test.assertEquals @@ -143,4 +144,11 @@ class MarketProcessorTests { assertEquals(output?.oraclePrice, 10000.0000) assertEquals(output?.priceChange24HPercent, 0.014011676210602023) } + + @Test + fun testProcessSparklines() { + processor.process("BTC-USD", marketPayloadMock) + val output = processor.processSparklines("BTC-USD", listOf("1", "2", "3")) + assertEquals(output?.perpetual?.line, listOf(3.0, 2.0, 1.0).toIList()) + } } diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/processor/markets/MarketsProcessorTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/processor/markets/MarketsProcessorTests.kt index 3abb66a22..82ab43d04 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/processor/markets/MarketsProcessorTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/processor/markets/MarketsProcessorTests.kt @@ -1,6 +1,5 @@ package exchange.dydx.abacus.processor.markets -import exchange.dydx.abacus.processor.wallet.account.HistoricalPNLProcessorTests.Companion.payload import exchange.dydx.abacus.state.internalstate.InternalMarketSummaryState import exchange.dydx.abacus.tests.mock.processor.markets.MarketProcessorMock import exchange.dydx.abacus.utils.Parser @@ -82,4 +81,26 @@ class MarketsProcessorTests { assertEquals(2, marketProcessor.processCallCount) assertEquals(2, marketProcessor.processOraclePriceCallCount) } + + @Test + fun testProcessSparklines() { + val state = InternalMarketSummaryState() + marketProcessor.processAction = { _, _ -> + MarketProcessorTests.outputMock + } + + val payload = mapOf( + "BTC-USD" to MarketProcessorTests.marketPayloadMock, + "ETH-USD" to MarketProcessorTests.marketPayloadMock, + ) + processor.processSubscribed(state, payload) + + val sparklines = mapOf( + "BTC-USD" to listOf("1", "2", "3"), + "ETH-USD" to listOf("1", "2", "3"), + ) + val result = processor.processSparklines(state, sparklines) + assertEquals(2, marketProcessor.processSparklinesCallCount) + assertEquals(2, result.markets.size) + } } diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/tests/mock/processor/markets/MarketProcessorMock.kt b/src/commonTest/kotlin/exchange.dydx.abacus/tests/mock/processor/markets/MarketProcessorMock.kt index 68c992241..a1afbc2aa 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/tests/mock/processor/markets/MarketProcessorMock.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/tests/mock/processor/markets/MarketProcessorMock.kt @@ -10,9 +10,11 @@ class MarketProcessorMock : MarketProcessorProtocol { var processAction: ((String, IndexerCompositeMarketObject) -> PerpetualMarket?)? = null var processOraclePriceAction: ((String, IndexerWsMarketOraclePriceObject) -> PerpetualMarket?)? = null var clearCachedOraclePriceAction: ((String) -> Unit)? = null + var processSparklinesAction: ((String, List) -> PerpetualMarket?)? = null var processCallCount = 0 var processOraclePriceCallCount = 0 var clearCachedOraclePriceCallCount = 0 + var processSparklinesCallCount = 0 override fun process( marketId: String, @@ -30,6 +32,11 @@ class MarketProcessorMock : MarketProcessorProtocol { return processOraclePriceAction?.invoke(marketId, payload) } + override fun processSparklines(marketId: String, payload: List): PerpetualMarket? { + processSparklinesCallCount++ + return processSparklinesAction?.invoke(marketId, payload) + } + override fun clearCachedOraclePrice(marketId: String) { clearCachedOraclePriceCallCount++ clearCachedOraclePriceAction?.invoke(marketId) From 0cea1684c9cdac499c4f55d9ddef4e73da839805 Mon Sep 17 00:00:00 2001 From: mobile-build-bot-git Date: Sat, 3 Aug 2024 01:10:23 +0000 Subject: [PATCH 05/63] Bump version --- build.gradle.kts | 2 +- v4_abacus.podspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 9899c055d..71ee6457e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -52,7 +52,7 @@ allprojects { } group = "exchange.dydx.abacus" -version = "1.8.76" +version = "1.8.77" repositories { google() diff --git a/v4_abacus.podspec b/v4_abacus.podspec index 1635ba18f..d61c9d1fd 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.76' + spec.version = '1.8.77' spec.homepage = 'https://github.com/dydxprotocol/v4-abacus' spec.source = { :http=> ''} spec.authors = '' From 3d6fbb657c82a1ef5ef8ddf40b63250c7806c6ab Mon Sep 17 00:00:00 2001 From: Rui Date: Fri, 2 Aug 2024 18:18:49 -0700 Subject: [PATCH 06/63] Lint --- .../kotlin/exchange.dydx.abacus/output/Market.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/output/Market.kt b/src/commonMain/kotlin/exchange.dydx.abacus/output/Market.kt index ca4397497..bad7965a1 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/output/Market.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/output/Market.kt @@ -839,7 +839,6 @@ data class MarketOrderbook( depending on the timing of v3_markets socket channel and /config/markets.json, the object may contain empty fields until both payloads are received and processed */ -@Suppress("UNCHECKED_CAST") @JsExport @Serializable data class PerpetualMarket( @@ -964,7 +963,7 @@ data class PerpetualMarketSummary( markets[marketId] = it } } - return perpetualMarketSummary(existing, parser, data, markets) + return createPerpetualMarketSummary(existing, parser, data, markets) } else { val marketsData = parser.asMap(data["markets"]) ?: return null val changedMarkets = changes.markets ?: marketsData.keys @@ -985,11 +984,11 @@ data class PerpetualMarketSummary( ) markets.typedSafeSet(marketId, perpMarket) } - return perpetualMarketSummary(existing, parser, data, markets) + return createPerpetualMarketSummary(existing, parser, data, markets) } } - private fun perpetualMarketSummary( + private fun createPerpetualMarketSummary( existing: PerpetualMarketSummary?, parser: ParserProtocol, data: Map, From e4c712c9c2134653f21876fc201edabfe302b176 Mon Sep 17 00:00:00 2001 From: Rui Date: Mon, 5 Aug 2024 09:40:21 -0700 Subject: [PATCH 07/63] WIP --- .../processor/markets/MarketProcessor.kt | 2 +- .../processor/markets/MarketsProcessor.kt | 4 +-- .../markets/MarketsSummaryProcessor.kt | 30 +++++++++++++------ .../processor/markets/OrderbookProcessor.kt | 14 +++++++++ .../model/TradingStateMachine+Orderbook.kt | 10 ++++++- 5 files changed, 47 insertions(+), 13 deletions(-) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketProcessor.kt index 24639a037..e33108707 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketProcessor.kt @@ -530,7 +530,7 @@ internal class MarketProcessor( return modified } - internal fun receivedOrderbook( + internal fun receivedOrderbookDeprecated( market: Map, payload: Map, ): Map { diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsProcessor.kt index b22b0dd03..baa23d18b 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsProcessor.kt @@ -220,14 +220,14 @@ internal class MarketsProcessor( return markets } - internal fun receivedOrderbook( + internal fun receivedOrderbookDeprecated( existing: Map?, market: String, payload: Map ): Map { val marketData = parser.asNativeMap(existing?.get(market)) ?: mutableMapOf() val markets = existing?.mutable() ?: mutableMapOf() - markets[market] = marketProcessorDeprecated!!.receivedOrderbook(marketData, payload) + markets[market] = marketProcessorDeprecated!!.receivedOrderbookDeprecated(marketData, payload) return markets } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsSummaryProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsSummaryProcessor.kt index 7be440593..0eb273396 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsSummaryProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsSummaryProcessor.kt @@ -5,26 +5,30 @@ import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.internalstate.InternalMarketSummaryState import exchange.dydx.abacus.utils.mutable import exchange.dydx.abacus.utils.safeSet +import indexer.codegen.IndexerOrderbookResponseObject import indexer.models.IndexerCompositeMarketObject import indexer.models.IndexerWsMarketUpdateResponse internal class MarketsSummaryProcessor( parser: ParserProtocol, - calculateSparklines: Boolean = false + calculateSparklines: Boolean = false, + private val staticTyping: Boolean, ) : BaseProcessor(parser) { private val marketsProcessor = MarketsProcessor(parser, calculateSparklines) + private val orderbookProcessor = OrderbookProcessor(parser) internal var groupingMultiplier: Int - get() = marketsProcessor.groupingMultiplier + get() = if (staticTyping) orderbookProcessor.groupingMultiplier else marketsProcessor.groupingMultiplier set(value) { - marketsProcessor.groupingMultiplier = value + if (staticTyping) orderbookProcessor.groupingMultiplier = value + else marketsProcessor.groupingMultiplier = value } fun processSubscribed( existing: InternalMarketSummaryState, content: Map?, ): InternalMarketSummaryState { - val markets = marketsProcessor.processSubscribed(existing, content) + marketsProcessor.processSubscribed(existing, content) return existing } @@ -32,7 +36,7 @@ internal class MarketsSummaryProcessor( existing: InternalMarketSummaryState, content: IndexerWsMarketUpdateResponse?, ): InternalMarketSummaryState { - val markets = marketsProcessor.processChannelData(existing, content) + marketsProcessor.processChannelData(existing, content) return existing } @@ -40,7 +44,7 @@ internal class MarketsSummaryProcessor( existing: InternalMarketSummaryState, content: List?, ): InternalMarketSummaryState { - val markets = marketsProcessor.processChannelBatchData(existing, content) + marketsProcessor.processChannelBatchData(existing, content) return existing } @@ -48,7 +52,15 @@ internal class MarketsSummaryProcessor( existing: InternalMarketSummaryState, content: Map>?, ): InternalMarketSummaryState { - val markets = marketsProcessor.processSparklines(existing, content) + marketsProcessor.processSparklines(existing, content) + return existing + } + + fun processOrderbook( + existing: InternalMarketSummaryState, + market: String, + content: IndexerOrderbookResponseObject?, + ): InternalMarketSummaryState { return existing } @@ -88,12 +100,12 @@ internal class MarketsSummaryProcessor( return modify(existing, markets)!! } - internal fun receivedOrderbook( + internal fun receivedOrderbookDeprecated( existing: Map?, market: String, payload: Map ): Map? { - val markets = marketsProcessor.receivedOrderbook( + val markets = marketsProcessor.receivedOrderbookDeprecated( parser.asNativeMap(existing?.get("markets")), market, payload, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/OrderbookProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/OrderbookProcessor.kt index 572627d28..c454cc53c 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/OrderbookProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/OrderbookProcessor.kt @@ -7,6 +7,7 @@ import exchange.dydx.abacus.utils.Numeric import exchange.dydx.abacus.utils.Rounder import exchange.dydx.abacus.utils.mutable import exchange.dydx.abacus.utils.safeSet +import indexer.codegen.IndexerOrderbookResponseObject import tickDecimals @Suppress("UNCHECKED_CAST") @@ -19,6 +20,19 @@ internal class OrderbookProcessor(parser: ParserProtocol) : BaseProcessor(parser private var lastOffset: Long = 0 + fun processSubscribed( + existing: Map, + content: IndexerOrderbookResponseObject?, + ): Map { + + return if (content != null) { + val orderbook = received(existing, content) + calculate(orderbook) + } else { + existing + } + } + internal fun subscribed( content: Map ): Map { diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Orderbook.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Orderbook.kt index 8c45ad8e8..fdfbaa389 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Orderbook.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Orderbook.kt @@ -1,8 +1,10 @@ package exchange.dydx.abacus.state.model +import exchange.dydx.abacus.protocols.asTypedObject import exchange.dydx.abacus.responses.StateResponse import exchange.dydx.abacus.state.changes.Changes import exchange.dydx.abacus.state.changes.StateChanges +import indexer.codegen.IndexerOrderbookResponseObject import kollections.iListOf internal fun TradingStateMachine.receivedOrderbook( @@ -11,7 +13,13 @@ internal fun TradingStateMachine.receivedOrderbook( subaccountNumber: Int ): StateChanges? { return if (market != null) { - this.marketsSummary = marketsProcessor.receivedOrderbook(marketsSummary, market, payload) + if (staticTyping) { + val orderbookPayload = parser.asTypedObject(payload) + print("orderbookPayload: $orderbookPayload") + } else { + this.marketsSummary = + marketsProcessor.receivedOrderbookDeprecated(marketsSummary, market, payload) + } StateChanges(iListOf(Changes.orderbook, Changes.input), iListOf(market), iListOf(subaccountNumber)) } else { null From f03b424b09ec947fdc81d6d3ec0e1975a3d9d05e Mon Sep 17 00:00:00 2001 From: Rui Date: Mon, 5 Aug 2024 19:54:21 -0700 Subject: [PATCH 08/63] Orderbook --- .../calculator/OrderbookCalculator.kt | 214 ++++++++++++++++++ .../exchange.dydx.abacus/output/Market.kt | 4 +- .../processor/markets/MarketProcessor.kt | 2 +- .../markets/MarketsSummaryProcessor.kt | 47 +++- .../processor/markets/OrderbookProcessor.kt | 172 ++++++++++++-- .../state/internalstate/InternalState.kt | 23 ++ .../model/TradingStateMachine+Orderbook.kt | 91 ++++++-- .../state/model/TradingStateMachine.kt | 27 ++- .../IndexerWsOrderbookUpdateResponse.kt | 18 ++ .../calculator/OrderbookCalculatorTests.kt | 161 +++++++++++++ .../exchange.dydx.abacus/payload/BaseTests.kt | 16 +- .../payload/v4/V4OrderbookTests.kt | 2 +- .../markets/OrderbookProcessorTests.kt | 149 ++++++++++++ .../calculator/OrderbookCalculatorMock.kt | 26 +++ 14 files changed, 891 insertions(+), 61 deletions(-) create mode 100644 src/commonMain/kotlin/exchange.dydx.abacus/calculator/OrderbookCalculator.kt create mode 100644 src/commonMain/kotlin/indexer/models/IndexerWsOrderbookUpdateResponse.kt create mode 100644 src/commonTest/kotlin/exchange.dydx.abacus/calculator/OrderbookCalculatorTests.kt create mode 100644 src/commonTest/kotlin/exchange.dydx.abacus/processor/markets/OrderbookProcessorTests.kt create mode 100644 src/commonTest/kotlin/exchange.dydx.abacus/tests/mock/calculator/OrderbookCalculatorMock.kt diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/OrderbookCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/OrderbookCalculator.kt new file mode 100644 index 000000000..b2dcb7900 --- /dev/null +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/OrderbookCalculator.kt @@ -0,0 +1,214 @@ +package exchange.dydx.abacus.calculator + +import exchange.dydx.abacus.output.MarketOrderbook +import exchange.dydx.abacus.output.MarketOrderbookGrouping +import exchange.dydx.abacus.output.OrderbookLine +import exchange.dydx.abacus.protocols.ParserProtocol +import exchange.dydx.abacus.state.internalstate.InternalOrderbook +import exchange.dydx.abacus.state.internalstate.InternalOrderbookTick +import exchange.dydx.abacus.state.manager.OrderbookGrouping +import exchange.dydx.abacus.utils.Numeric +import exchange.dydx.abacus.utils.Rounder +import kollections.toIList +import tickDecimals + +internal interface OrderbookCalculatorProtocol { + // Creates the orderbook entries for app display + fun calculate( + rawOrderbook: InternalOrderbook?, + tickSize: Double, + groupingMultiplier: Int, + ): MarketOrderbook? + + // Creates the orderbook entries for trade calculations + fun consolidate( + rawOrderbook: InternalOrderbook?, + ): InternalOrderbook? +} + +internal class OrderbookCalculator( + private val parser: ParserProtocol, +) : OrderbookCalculatorProtocol { + private var groupingTickSize: Double? = null + private var groupingLookup: MutableMap? = null + + override fun calculate( + rawOrderbook: InternalOrderbook?, + tickSize: Double, + groupingMultiplier: Int, + ): MarketOrderbook? { + if (rawOrderbook == null) { + return null + } + + buildGroupingLookup(tickSize) + val groupingTickSize = getGroupingTickSizeDecimals(tickSize, groupingMultiplier) + val asks = createGroup(rawOrderbook.asks, groupingTickSize) + val bids = createGroup(rawOrderbook.bids, groupingTickSize) + + val firstAskPrice = asks?.firstOrNull()?.price + val firstBidPrice = bids?.firstOrNull()?.price + + val multiplier = OrderbookGrouping.invoke(groupingMultiplier) + if (firstAskPrice != null && firstBidPrice != null) { + val midPrice = (firstAskPrice + firstBidPrice) / 2.0 + val spread = firstAskPrice - firstBidPrice + val spreadPercent = spread / midPrice + return MarketOrderbook( + midPrice = midPrice, + spreadPercent = spreadPercent, + spread = spread, + grouping = multiplier?.let { + MarketOrderbookGrouping( + tickSize = groupingTickSize, + multiplier = it, + ) + }, + asks = asks.toIList(), + bids = bids.toIList(), + ) + } else { + return MarketOrderbook( + midPrice = null, + spreadPercent = null, + spread = null, + grouping = multiplier?.let { + MarketOrderbookGrouping( + tickSize = groupingTickSize, + multiplier = it, + ) + }, + asks = asks?.toIList(), + bids = bids?.toIList(), + ) + } + } + + override fun consolidate( + rawOrderbook: InternalOrderbook?, + ): InternalOrderbook? { + val asks = rawOrderbook?.asks?.toMutableList() + val bids = rawOrderbook?.bids?.toMutableList() + return if (asks != null && bids != null && asks.size > 0 && bids.size > 0) { + var ask = asks.firstOrNull() + var bid = bids.firstOrNull() + while (ask != null && bid != null && crossed(ask, bid)) { + val askSize = ask.size + val bidSize = bid.size + if (askSize >= bidSize) { + bids.removeFirst() + bid = bids.firstOrNull() + } else { + asks.removeFirst() + ask = asks.firstOrNull() + } + } + InternalOrderbook( + asks = asks, + bids = bids, + ) + } else { + rawOrderbook + } + } + + private fun crossed(ask: InternalOrderbookTick, bid: InternalOrderbookTick): Boolean { + val askPrice = ask.price + val bidPrice = bid.price + return askPrice <= bidPrice + } + + private fun createGroup( + orderbookTicks: List?, + grouping: Double, + ): List? { + return if (!orderbookTicks.isNullOrEmpty()) { + // orderbook always ordered in increasing depth which is either increasing (asks) or decreasing (bids) price + // we want to round asks up and bids down so they don't have an overlapping group in the middle + val firstPrice = orderbookTicks.first().price + val lastPrice = orderbookTicks.last().price + val shouldFloor = lastPrice <= firstPrice + val result = mutableListOf() + + // properties of the current group + var curFloored = Rounder.round(firstPrice, grouping); + var groupMin = if (curFloored != firstPrice) curFloored else (if (shouldFloor) curFloored else curFloored - grouping) + var groupMax = groupMin + grouping + var size = Numeric.double.ZERO + var sizeCost = Numeric.double.ZERO + var depth = Numeric.double.ZERO + var depthCost = Numeric.double.ZERO + + for (item in orderbookTicks) { + val linePrice = item.price + val lineSize = item.size + val lineSizeCost = lineSize * linePrice + + // if in this group + // remember: if flooring then min inclusive max exclusive; if ceiling then min exclusive, max inclusive + if ((linePrice > groupMin && linePrice < groupMax) || (linePrice == groupMin && shouldFloor) || (linePrice == groupMax && !shouldFloor)) { + size += lineSize + sizeCost += lineSizeCost + depth += lineSize + depthCost += lineSizeCost + } else { + result.add( + OrderbookLine( + size = size, + price = if (shouldFloor) groupMin else groupMax, + depth = depth, + sizeCost = sizeCost, + depthCost = depthCost, + ), + ) + curFloored = Rounder.round(linePrice, grouping); + groupMin = if (curFloored != linePrice) curFloored else (if (shouldFloor) curFloored else curFloored - grouping) + groupMax = groupMin + grouping + + size = lineSize + sizeCost = lineSizeCost + depth += lineSize + depthCost += lineSizeCost + } + } + result.add( + OrderbookLine( + size = size, + price = if (shouldFloor) groupMin else groupMax, + depth = depth, + sizeCost = sizeCost, + depthCost = depthCost, + ), + ) + return result + } else { + null + } + } + + private fun buildGroupingLookup(tickSize: Double) { + if (groupingTickSize != tickSize) { + groupingTickSize = tickSize + groupingLookup = mutableMapOf() + } + } + + private fun getGroupingTickSizeDecimals(tickSize: Double, groupingMultiplier: Int): Double { + val cached = groupingLookup?.get(groupingMultiplier) + return if (cached != null) { + cached + } else { + val decimals = if (groupingMultiplier == 1) { + parser.asDouble(tickSize.tickDecimals())!! + } else { + val tickDecimals = parser.asDouble(tickSize.tickDecimals())!! + tickDecimals * groupingMultiplier + } + if (groupingLookup == null) { + groupingLookup = mutableMapOf() + } + groupingLookup?.set(groupingMultiplier, decimals) + decimals + } + } +} diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/output/Market.kt b/src/commonMain/kotlin/exchange.dydx.abacus/output/Market.kt index bad7965a1..2656f509f 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/output/Market.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/output/Market.kt @@ -494,7 +494,6 @@ data class MarketCandle( /* "1MIN", "5MINS", "15MINS", "30MINS", "1HOUR", "4HOURS", "1DAY" */ -@Suppress("UNCHECKED_CAST") @JsExport @Serializable data class MarketCandles( @@ -656,7 +655,6 @@ data class MarketTrade( } } -@Suppress("UNCHECKED_CAST") @JsExport @Serializable data class OrderbookLine( @@ -707,7 +705,7 @@ Under extreme conditions, orderbook may be obsent, or one-sided */ @JsExport -@kotlinx.serialization.Serializable +@Serializable data class MarketOrderbookGrouping(val multiplier: OrderbookGrouping, val tickSize: Double?) { companion object { internal fun create( diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketProcessor.kt index e33108707..1e36cfccf 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketProcessor.kt @@ -534,7 +534,7 @@ internal class MarketProcessor( market: Map, payload: Map, ): Map { - val orderbookRaw = orderbookProcessor.subscribed(payload) + val orderbookRaw = orderbookProcessor.subscribedDeprecated(payload) return processRawOrderbook(market, orderbookRaw) } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsSummaryProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsSummaryProcessor.kt index 0eb273396..fec15333d 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsSummaryProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsSummaryProcessor.kt @@ -2,12 +2,14 @@ package exchange.dydx.abacus.processor.markets import exchange.dydx.abacus.processor.base.BaseProcessor import exchange.dydx.abacus.protocols.ParserProtocol +import exchange.dydx.abacus.state.internalstate.InternalMarketState import exchange.dydx.abacus.state.internalstate.InternalMarketSummaryState import exchange.dydx.abacus.utils.mutable import exchange.dydx.abacus.utils.safeSet import indexer.codegen.IndexerOrderbookResponseObject import indexer.models.IndexerCompositeMarketObject import indexer.models.IndexerWsMarketUpdateResponse +import indexer.models.IndexerWsOrderbookUpdateResponse internal class MarketsSummaryProcessor( parser: ParserProtocol, @@ -21,7 +23,7 @@ internal class MarketsSummaryProcessor( get() = if (staticTyping) orderbookProcessor.groupingMultiplier else marketsProcessor.groupingMultiplier set(value) { if (staticTyping) orderbookProcessor.groupingMultiplier = value - else marketsProcessor.groupingMultiplier = value + marketsProcessor.groupingMultiplier = value } fun processSubscribed( @@ -58,9 +60,46 @@ internal class MarketsSummaryProcessor( fun processOrderbook( existing: InternalMarketSummaryState, - market: String, + tickSize: Double?, + marketId: String, content: IndexerOrderbookResponseObject?, ): InternalMarketSummaryState { + val marketState = existing.markets[marketId] ?: InternalMarketState() + existing.markets[marketId] = orderbookProcessor.processSubscribed( + existing = marketState, + tickSize = tickSize, + content = content, + ) + return existing + } + + fun processBatchOrderbookChanges( + existing: InternalMarketSummaryState, + tickSize: Double?, + marketId: String, + content: List?, + ): InternalMarketSummaryState { + val marketState = existing.markets[marketId] ?: InternalMarketState() + existing.markets[marketId] = orderbookProcessor.processChannelBatchData( + existing = marketState, + tickSize = tickSize, + content = content, + ) + return existing + } + + fun groupOrderbook( + existing: InternalMarketSummaryState, + tickSize: Double?, + marketId: String, + groupingMultiplier: Int, + ): InternalMarketSummaryState { + val marketState = existing.markets[marketId] ?: InternalMarketState() + existing.markets[marketId] = orderbookProcessor.processGrouping( + existing = marketState, + tickSize = tickSize, + groupingMultiplier = groupingMultiplier, + ) return existing } @@ -113,7 +152,7 @@ internal class MarketsSummaryProcessor( return modify(existing, markets) } - internal fun receivedBatchOrderbookChanges( + internal fun receivedBatchOrderbookChangesDeprecated( existing: Map?, market: String, payload: List @@ -255,7 +294,7 @@ internal class MarketsSummaryProcessor( } } - internal fun groupOrderbook(existing: Map?, market: String?): Map? { + internal fun groupOrderbookDeprecated(existing: Map?, market: String?): Map? { val markets = marketsProcessor.groupOrderbook(parser.asNativeMap(existing?.get("markets")), market) return modify(existing, markets) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/OrderbookProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/OrderbookProcessor.kt index c454cc53c..fed1c1c31 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/OrderbookProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/OrderbookProcessor.kt @@ -1,17 +1,30 @@ package exchange.dydx.abacus.processor.markets +import exchange.dydx.abacus.calculator.OrderbookCalculator +import exchange.dydx.abacus.calculator.OrderbookCalculatorProtocol import exchange.dydx.abacus.processor.base.BaseProcessor import exchange.dydx.abacus.processor.base.ComparisonOrder import exchange.dydx.abacus.protocols.ParserProtocol +import exchange.dydx.abacus.state.internalstate.InternalMarketState +import exchange.dydx.abacus.state.internalstate.InternalOrderbook +import exchange.dydx.abacus.state.internalstate.InternalOrderbookTick import exchange.dydx.abacus.utils.Numeric import exchange.dydx.abacus.utils.Rounder import exchange.dydx.abacus.utils.mutable import exchange.dydx.abacus.utils.safeSet import indexer.codegen.IndexerOrderbookResponseObject +import indexer.codegen.IndexerOrderbookResponsePriceLevel +import indexer.models.IndexerWsOrderbookUpdateItem +import indexer.models.IndexerWsOrderbookUpdateResponse +import indexer.models.getPrice +import indexer.models.getSize import tickDecimals @Suppress("UNCHECKED_CAST") -internal class OrderbookProcessor(parser: ParserProtocol) : BaseProcessor(parser) { +internal class OrderbookProcessor( + parser: ParserProtocol, + private val calculator: OrderbookCalculatorProtocol = OrderbookCalculator(parser), +) : BaseProcessor(parser) { private var entryProcessor = OrderbookEntryProcessor(parser = parser) internal var groupingMultiplier: Int = 1 @@ -21,19 +34,100 @@ internal class OrderbookProcessor(parser: ParserProtocol) : BaseProcessor(parser private var lastOffset: Long = 0 fun processSubscribed( - existing: Map, + existing: InternalMarketState, + tickSize: Double?, content: IndexerOrderbookResponseObject?, - ): Map { + ): InternalMarketState { + existing.rawOrderbook = InternalOrderbook( + asks = content?.asks?.mapNotNull { + createTick(it) + }, + bids = content?.bids?.mapNotNull { + createTick(it) + }, + ) + existing.consolidatedOrderbook = calculator.consolidate( + rawOrderbook = existing.rawOrderbook, + ) + existing.groupedOrderbook = calculator.calculate( + rawOrderbook = existing.rawOrderbook, + tickSize = tickSize ?: 0.1, + groupingMultiplier = groupingMultiplier, + ) + return existing + } - return if (content != null) { - val orderbook = received(existing, content) - calculate(orderbook) + fun processChannelBatchData( + existing: InternalMarketState, + tickSize: Double?, + content: List?, + ): InternalMarketState { + content?.forEach { + processChannelData(existing, tickSize, it) + } + return existing + } + + fun processGrouping( + existing: InternalMarketState, + tickSize: Double?, + groupingMultiplier: Int, + ): InternalMarketState { + this.groupingMultiplier = groupingMultiplier + existing.groupedOrderbook = calculator.calculate( + rawOrderbook = existing.rawOrderbook, + tickSize = tickSize ?: 0.1, + groupingMultiplier = groupingMultiplier, + ) + return existing + } + + private fun processChannelData( + existing: InternalMarketState, + tickSize: Double?, + content: IndexerWsOrderbookUpdateResponse?, + ): InternalMarketState { + existing.rawOrderbook = InternalOrderbook( + asks = processChangesBinary( + existing = existing.rawOrderbook?.asks, + changes = content?.asks, + ascending = true, + ), + bids = processChangesBinary( + existing = existing.rawOrderbook?.bids, + changes = content?.bids, + ascending = false, + ), + ) + existing.consolidatedOrderbook = calculator.consolidate( + rawOrderbook = existing.rawOrderbook, + ) + existing.groupedOrderbook = calculator.calculate( + rawOrderbook = existing.rawOrderbook, + tickSize = tickSize ?: 0.1, + groupingMultiplier = groupingMultiplier, + ) + return existing + } + + private fun createTick(payload: IndexerOrderbookResponsePriceLevel): InternalOrderbookTick? { + val price = parser.asDouble(payload.price) + val size = parser.asDouble(payload.size) + return createTick(price, size) + } + + private fun createTick(price: Double?, size: Double?): InternalOrderbookTick? { + return if (price != null && size != null) { + InternalOrderbookTick( + price = price, + size = size, + ) } else { - existing + null } } - internal fun subscribed( + internal fun subscribedDeprecated( content: Map ): Map { return received(null, content) @@ -93,14 +187,14 @@ internal class OrderbookProcessor(parser: ParserProtocol) : BaseProcessor(parser val orderbook = existing?.mutable() ?: mutableMapOf() // offset in v4 is always null. We just increment our own offset val offset = parser.asLong(payload["offset"]) ?: (lastOffset + 1) - orderbook["asks"] = receivedChanges( + orderbook["asks"] = receivedChangesDeprecated( orderbook["asks"] as? List>, parser.asNativeList(payload["asks"] ?: payload["ask"]), offset, true, ) - orderbook["bids"] = receivedChanges( + orderbook["bids"] = receivedChangesDeprecated( orderbook["bids"] as? List>, parser.asNativeList(payload["bids"] ?: payload["bid"]), offset, @@ -112,7 +206,7 @@ internal class OrderbookProcessor(parser: ParserProtocol) : BaseProcessor(parser return existing } - private fun receivedChanges( + private fun receivedChangesDeprecated( existing: List>?, changes: List?, offset: Long?, @@ -122,7 +216,7 @@ internal class OrderbookProcessor(parser: ParserProtocol) : BaseProcessor(parser return existing ?: mutableListOf() } - val bindaryResult = receivedChangesBinary(existing, changes, offset, ascending) + val bindaryResult = receivedChangesBinaryDeprecated(existing, changes, offset, ascending) /* k = Number of channel_data contained in channel_batched_data g = Number of changed items in channel_data @@ -144,7 +238,53 @@ internal class OrderbookProcessor(parser: ParserProtocol) : BaseProcessor(parser return bindaryResult } - private fun receivedChangesBinary( + private fun processChangesBinary( + existing: List?, + changes: List?, + ascending: Boolean + ): List { + val comparator = compareBy { + val price = it.price + if (ascending) price else (price * Numeric.double.NEGATIVE) + } + + val orderbook = existing?.mutable() ?: mutableListOf() + for (change in changes ?: emptyList()) { + processChangeBinary(orderbook, change, comparator) + } + return orderbook + } + + private fun processChangeBinary( + orderbook: MutableList, + change: IndexerWsOrderbookUpdateItem, + comparator: Comparator, + ) { + val price = change.getPrice(parser) + val size = change.getSize(parser) + if (price != null && size != null) { + val item = InternalOrderbookTick( + price = price, + size = size, + ) + val index = orderbook.binarySearch(item, comparator) + if (index >= 0) { + // found the item + val existing = orderbook[index] + orderbook.removeAt(index) + if (size != Numeric.double.ZERO) { + orderbook.add(index, item) + } + } else { + if (size != Numeric.double.ZERO) { + val insertionIndex = (index + 1) * -1 + orderbook.add(insertionIndex, item) + } + } + } + } + + private fun receivedChangesBinaryDeprecated( existing: List>?, changes: List, offset: Long?, @@ -160,12 +300,12 @@ internal class OrderbookProcessor(parser: ParserProtocol) : BaseProcessor(parser } var orderbook = existing?.mutable() ?: mutableListOf() for (change in changes) { - orderbook = receiveChangeBinary(orderbook, change, offset, comparator) + orderbook = receiveChangeBinaryDeprecated(orderbook, change, offset, comparator) } return orderbook } - private fun receiveChangeBinary( + private fun receiveChangeBinaryDeprecated( existing: List>, change: Any, offset: Long?, @@ -530,7 +670,7 @@ internal class OrderbookProcessor(parser: ParserProtocol) : BaseProcessor(parser } } - fun group(orderbook: List?, grouping: Double): List? { + private fun group(orderbook: List?, grouping: Double): List? { return if (!orderbook.isNullOrEmpty()) { // orderbook always ordered in increasing depth which is either increasing (asks) or decreasing (bids) price // we want to round asks up and bids down so they don't have an overlapping group in the middle diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt index 04716b842..b6a7bd18e 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt @@ -7,6 +7,7 @@ import exchange.dydx.abacus.output.EquityTiers import exchange.dydx.abacus.output.FeeTier import exchange.dydx.abacus.output.LaunchIncentivePoint import exchange.dydx.abacus.output.LaunchIncentiveSeason +import exchange.dydx.abacus.output.MarketOrderbook import exchange.dydx.abacus.output.MarketTrade import exchange.dydx.abacus.output.PerpetualMarket import exchange.dydx.abacus.output.WithdrawalGating @@ -42,8 +43,30 @@ internal data class InternalMarketSummaryState( ) internal data class InternalMarketState( + // recent trades var trades: List? = null, + + // market details var perpetualMarket: PerpetualMarket? = null, + + // raw orderbook + var rawOrderbook: InternalOrderbook? = null, + + // orderbook for trade calculations + var consolidatedOrderbook: InternalOrderbook? = null, + + // grouped orderbook for app display + var groupedOrderbook: MarketOrderbook? = null, +) + +internal data class InternalOrderbook( + val asks: List? = null, + val bids: List? = null, +) + +internal data class InternalOrderbookTick( + val price: Double, + val size: Double, ) internal data class InternalConfigsState( diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Orderbook.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Orderbook.kt index fdfbaa389..1f2806fb1 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Orderbook.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Orderbook.kt @@ -1,29 +1,42 @@ package exchange.dydx.abacus.state.model +import exchange.dydx.abacus.protocols.asTypedList import exchange.dydx.abacus.protocols.asTypedObject import exchange.dydx.abacus.responses.StateResponse import exchange.dydx.abacus.state.changes.Changes import exchange.dydx.abacus.state.changes.StateChanges import indexer.codegen.IndexerOrderbookResponseObject +import indexer.models.IndexerWsOrderbookUpdateResponse import kollections.iListOf internal fun TradingStateMachine.receivedOrderbook( - market: String?, + marketId: String?, payload: Map, subaccountNumber: Int ): StateChanges? { - return if (market != null) { - if (staticTyping) { - val orderbookPayload = parser.asTypedObject(payload) - print("orderbookPayload: $orderbookPayload") - } else { - this.marketsSummary = - marketsProcessor.receivedOrderbookDeprecated(marketsSummary, market, payload) - } - StateChanges(iListOf(Changes.orderbook, Changes.input), iListOf(market), iListOf(subaccountNumber)) - } else { - null + if (marketId == null) { + return null + } + if (staticTyping) { + val orderbookPayload = parser.asTypedObject(payload) + val market = internalState.marketsSummary.markets[marketId] + marketsProcessor.processOrderbook( + existing = internalState.marketsSummary, + marketId = marketId, + tickSize = market?.perpetualMarket?.configs?.tickSize, + content = orderbookPayload, + ) } + + // TODO: Remove after TradeCalculator is converted to static typing + this.marketsSummary = + marketsProcessor.receivedOrderbookDeprecated(marketsSummary, marketId, payload) + + return StateChanges( + iListOf(Changes.orderbook, Changes.input), + iListOf(marketId), + iListOf(subaccountNumber), + ) } internal fun TradingStateMachine.receivedOrderbookChanges( @@ -40,25 +53,61 @@ internal fun TradingStateMachine.receivedOrderbookChanges( } internal fun TradingStateMachine.receivedBatchOrderbookChanges( - market: String?, + marketId: String?, payload: List, subaccountNumber: Int ): StateChanges? { - return if (market != null) { - this.marketsSummary = marketsProcessor.receivedBatchOrderbookChanges(marketsSummary, market, payload) - StateChanges(iListOf(Changes.orderbook, Changes.input), iListOf(market), iListOf(subaccountNumber)) - } else { - null + if (marketId == null) { + return null } + if (staticTyping) { + val orderbookUpdatePayload = parser.asTypedList(payload) + val market = internalState.marketsSummary.markets[marketId] + marketsProcessor.processBatchOrderbookChanges( + existing = internalState.marketsSummary, + tickSize = market?.perpetualMarket?.configs?.tickSize, + marketId = marketId, + content = orderbookUpdatePayload, + ) + } + // TODO: Remove after TradeCalculator is converted to static typing + this.marketsSummary = marketsProcessor.receivedBatchOrderbookChangesDeprecated( + marketsSummary, + marketId, + payload, + ) + return StateChanges( + iListOf(Changes.orderbook, Changes.input), + iListOf(marketId), + iListOf(subaccountNumber), + ) } -internal fun TradingStateMachine.setOrderbookGrouping(market: String?, groupingMultiplier: Int): StateResponse { +internal fun TradingStateMachine.setOrderbookGrouping( + marketId: String, + groupingMultiplier: Int, +): StateResponse { return if (this.groupingMultiplier != groupingMultiplier) { + if (staticTyping) { + val market = internalState.marketsSummary.markets[marketId] + marketsProcessor.groupOrderbook( + existing = internalState.marketsSummary, + tickSize = market?.perpetualMarket?.configs?.tickSize, + marketId = marketId, + groupingMultiplier = groupingMultiplier, + ) + } + + // TODO: Remove after TradeCalculator is converted to static typing this.groupingMultiplier = groupingMultiplier - this.marketsSummary = marketsProcessor.groupOrderbook(marketsSummary, market) + this.marketsSummary = marketsProcessor.groupOrderbookDeprecated(marketsSummary, marketId) val changes = - StateChanges(iListOf(Changes.orderbook), if (market != null) iListOf(market) else null, null) + StateChanges( + iListOf(Changes.orderbook), + if (marketId != null) iListOf(marketId) else null, + null, + ) changes.let { update(it) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt index 0e1f54ca5..b5d737037 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt @@ -98,7 +98,10 @@ open class TradingStateMachine( internal var internalState: InternalState = InternalState() internal val parser: ParserProtocol = Parser() - internal val marketsProcessor = MarketsSummaryProcessor(parser) + internal val marketsProcessor = MarketsSummaryProcessor( + parser = parser, + staticTyping = staticTyping, + ) internal val tradesProcessorV2 = TradesProcessorV2(TradeProcessorV2(parser, localizer)) internal val assetsProcessor = run { val processor = AssetsProcessor( @@ -1112,15 +1115,19 @@ open class TradingStateMachine( orderbooks = if (markets != null) { val modified = orderbooks?.toIMutableMap() ?: iMutableMapOf() for (marketId in markets) { - val data = - parser.asNativeMap( - parser.value( - data, - "markets.markets.$marketId.orderbook", - ), - ) - val existing = orderbooks?.get(marketId) - val orderbook = MarketOrderbook.create(existing, parser, data) + val orderbook = if (staticTyping) { + internalState.marketsSummary.markets[marketId]?.groupedOrderbook + } else { + val data = + parser.asNativeMap( + parser.value( + data, + "markets.markets.$marketId.orderbook", + ), + ) + val existing = orderbooks?.get(marketId) + MarketOrderbook.create(existing, parser, data) + } modified.typedSafeSet(marketId, orderbook) } modified diff --git a/src/commonMain/kotlin/indexer/models/IndexerWsOrderbookUpdateResponse.kt b/src/commonMain/kotlin/indexer/models/IndexerWsOrderbookUpdateResponse.kt new file mode 100644 index 000000000..6166f53a6 --- /dev/null +++ b/src/commonMain/kotlin/indexer/models/IndexerWsOrderbookUpdateResponse.kt @@ -0,0 +1,18 @@ +package indexer.models + +import exchange.dydx.abacus.protocols.ParserProtocol +import kotlinx.serialization.Serializable + +@Serializable +data class IndexerWsOrderbookUpdateResponse( + val asks: List? = null, + val bids: List? = null, +) + +typealias IndexerWsOrderbookUpdateItem = List + +fun IndexerWsOrderbookUpdateItem.getPrice(parser: ParserProtocol): Double? = + parser.asDouble(this.getOrNull(0)) + +fun IndexerWsOrderbookUpdateItem.getSize(parser: ParserProtocol): Double? = + parser.asDouble(this.getOrNull(1)) diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/calculator/OrderbookCalculatorTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/calculator/OrderbookCalculatorTests.kt new file mode 100644 index 000000000..af0686fbd --- /dev/null +++ b/src/commonTest/kotlin/exchange.dydx.abacus/calculator/OrderbookCalculatorTests.kt @@ -0,0 +1,161 @@ +package exchange.dydx.abacus.calculator + +import exchange.dydx.abacus.output.MarketOrderbook +import exchange.dydx.abacus.output.MarketOrderbookGrouping +import exchange.dydx.abacus.output.OrderbookLine +import exchange.dydx.abacus.state.internalstate.InternalOrderbook +import exchange.dydx.abacus.state.internalstate.InternalOrderbookTick +import exchange.dydx.abacus.state.manager.OrderbookGrouping +import exchange.dydx.abacus.utils.Parser +import kollections.iListOf +import kotlin.test.Test +import kotlin.test.assertEquals + +class OrderbookCalculatorTests { + + companion object { + internal val rawOrderbook = InternalOrderbook( + asks = listOf( + InternalOrderbookTick( + price = 1000.1, + size = 1.0, + ), + InternalOrderbookTick( + price = 1000.5, + size = 2.0, + ), + InternalOrderbookTick( + price = 1060.0, + size = 5.0, + ), + InternalOrderbookTick( + price = 1800.0, + size = 10.0, + ), + ), + bids = listOf( + InternalOrderbookTick( + price = 999.9, + size = 1.0, + ), + InternalOrderbookTick( + price = 999.5, + size = 2.0, + ), + InternalOrderbookTick( + price = 990.5, + size = 5.0, + ), + InternalOrderbookTick( + price = 900.0, + size = 10.0, + ), + ), + ) + + val marketOrderbook = MarketOrderbook( + midPrice = 1000.0, + spread = 0.2, + spreadPercent = 0.0002, + asks = iListOf( + OrderbookLine( + price = 1000.1, + size = 1.0, + sizeCost = 1000.1, + depth = 1.0, + depthCost = 1000.1, + ), + OrderbookLine( + price = 1000.5, + size = 2.0, + sizeCost = 2001.0, + depth = 3.0, + depthCost = 3001.1, + ), + OrderbookLine( + price = 1060.0, + size = 5.0, + sizeCost = 5300.0, + depth = 8.0, + depthCost = 8301.1, + ), + OrderbookLine( + price = 1800.0, + size = 10.0, + sizeCost = 18000.0, + depth = 18.0, + depthCost = 26301.1, + ), + ), + bids = iListOf( + OrderbookLine( + price = 999.9, + size = 1.0, + sizeCost = 999.9, + depth = 1.0, + depthCost = 999.9, + ), + OrderbookLine( + price = 999.5, + size = 2.0, + sizeCost = 1999.0, + depth = 3.0, + depthCost = 2998.9, + ), + OrderbookLine( + price = 990.5, + size = 5.0, + sizeCost = 4952.5, + depth = 8.0, + depthCost = 7951.4, + ), + OrderbookLine( + price = 900.0, + size = 10.0, + sizeCost = 9000.0, + depth = 18.0, + depthCost = 16951.4, + ), + ), + grouping = MarketOrderbookGrouping( + multiplier = OrderbookGrouping.none, + tickSize = 0.1, + ), + ) + } + + private val calculator = OrderbookCalculator( + parser = Parser(), + ) + + @Test + fun testCalculate() { + val result = calculator.calculate( + rawOrderbook = rawOrderbook, + tickSize = 0.1, + groupingMultiplier = 1, + ) + requireNotNull(result) + assertEquals(marketOrderbook.asks, result.asks) + assertEquals(marketOrderbook.bids, result.bids) + assertEquals(marketOrderbook.midPrice!!, result.midPrice!!, 0.0001) + assertEquals(marketOrderbook.spread!!, result.spread!!, 0.0001) + assertEquals(marketOrderbook.spreadPercent!!, result.spreadPercent!!, 0.0001) + } + + @Test + fun testConsolidate() { + val crossedItem = InternalOrderbookTick( + price = 999.1, + size = 1.0, + ) + val asks = listOf(crossedItem) + rawOrderbook.asks!! + val result = calculator.consolidate( + rawOrderbook = rawOrderbook.copy( + asks = asks, + ), + ) + requireNotNull(result) + assertEquals(result.bids?.size, 3) + } +} diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/payload/BaseTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/payload/BaseTests.kt index 9874e7bd0..4fab80d0d 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/payload/BaseTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/payload/BaseTests.kt @@ -300,11 +300,17 @@ open class BaseTests( state?.candles, "candles", ) - verifyMarketsOrderbookState( - parser.asNativeMap(perp.marketsSummary?.get("markets")), - state?.orderbooks, - "orderbooks", - ) + if (staticTyping) { + for ((key, value) in perp.internalState.marketsSummary.markets) { + assertEquals(value.groupedOrderbook, state?.orderbooks?.get(key)) + } + } else { + verifyMarketsOrderbookState( + parser.asNativeMap(perp.marketsSummary?.get("markets")), + state?.orderbooks, + "orderbooks", + ) + } verifyInputState(perp.input, state?.input, "input") } diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4OrderbookTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4OrderbookTests.kt index 387165088..c438a6cba 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4OrderbookTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4OrderbookTests.kt @@ -12,7 +12,7 @@ class V4OrderbookTests : V3BaseTests() { AbUrl.fromString("wss://indexer.v4staging.dydx.exchange/v4/ws") @Test - fun testCandles() { + fun testOrderbook() { loadMarkets() loadMarketsConfigurations() diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/processor/markets/OrderbookProcessorTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/processor/markets/OrderbookProcessorTests.kt new file mode 100644 index 000000000..1ea78f54d --- /dev/null +++ b/src/commonTest/kotlin/exchange.dydx.abacus/processor/markets/OrderbookProcessorTests.kt @@ -0,0 +1,149 @@ +package exchange.dydx.abacus.processor.markets + +import exchange.dydx.abacus.state.internalstate.InternalMarketState +import exchange.dydx.abacus.state.internalstate.InternalOrderbook +import exchange.dydx.abacus.state.internalstate.InternalOrderbookTick +import exchange.dydx.abacus.tests.mock.calculator.OrderbookCalculatorMock +import exchange.dydx.abacus.utils.Parser +import indexer.codegen.IndexerOrderbookResponseObject +import indexer.codegen.IndexerOrderbookResponsePriceLevel +import indexer.models.IndexerWsOrderbookUpdateResponse +import kotlin.test.Test +import kotlin.test.assertEquals + +class OrderbookProcessorTests { + companion object { + internal val orderbookPayloadMock = IndexerOrderbookResponseObject( + asks = arrayOf( + IndexerOrderbookResponsePriceLevel(price = "1000.1", size = "1.0"), + IndexerOrderbookResponsePriceLevel(price = "1000.5", size = "2.0"), + IndexerOrderbookResponsePriceLevel(price = "1060.0", size = "5.0"), + IndexerOrderbookResponsePriceLevel(price = "1800.0", size = "10.0"), + ), + bids = arrayOf( + IndexerOrderbookResponsePriceLevel(price = "999.9", size = "1.0"), + IndexerOrderbookResponsePriceLevel(price = "999.5", size = "2.0"), + IndexerOrderbookResponsePriceLevel(price = "990.5", size = "5.0"), + IndexerOrderbookResponsePriceLevel(price = "900.0", size = "10.0"), + ), + ) + + internal val rawOrderbookResult = InternalOrderbook( + asks = listOf( + InternalOrderbookTick( + price = 1000.1, + size = 1.0, + ), + InternalOrderbookTick( + price = 1000.5, + size = 2.0, + ), + InternalOrderbookTick( + price = 1060.0, + size = 5.0, + ), + InternalOrderbookTick( + price = 1800.0, + size = 10.0, + ), + ), + bids = listOf( + InternalOrderbookTick( + price = 999.9, + size = 1.0, + ), + InternalOrderbookTick( + price = 999.5, + size = 2.0, + ), + InternalOrderbookTick( + price = 990.5, + size = 5.0, + ), + InternalOrderbookTick( + price = 900.0, + size = 10.0, + ), + ), + ) + } + + private val calculator = OrderbookCalculatorMock() + private val orderbookProcessor = OrderbookProcessor( + parser = Parser(), + calculator = calculator, + ) + + @Test + fun testProcessSubscribed() { + val state = InternalMarketState() + val result = orderbookProcessor.processSubscribed( + existing = state, + tickSize = 0.1, + content = orderbookPayloadMock, + ) + assertEquals(rawOrderbookResult, result.rawOrderbook) + assertEquals(calculator.consolidateCallCount, 1) + assertEquals(calculator.calculateCallCount, 1) + } + + @Test + fun testProcessChannelBatchData() { + val state = InternalMarketState() + var result = orderbookProcessor.processSubscribed( + existing = state, + tickSize = 0.1, + content = orderbookPayloadMock, + ) + assertEquals(calculator.consolidateCallCount, 1) + assertEquals(calculator.calculateCallCount, 1) + + result = orderbookProcessor.processChannelBatchData( + existing = result, + tickSize = 0.1, + content = listOf( + IndexerWsOrderbookUpdateResponse( + asks = listOf( + listOf("1000.2", "1.0"), + ), + bids = null, + ), + ), + ) + val asks = rawOrderbookResult.asks?.toMutableList() + asks?.add( + 1, + InternalOrderbookTick( + price = 1000.2, + size = 1.0, + ), + ) + assertEquals( + rawOrderbookResult.copy( + asks = asks, + ), + result.rawOrderbook, + ) + assertEquals(calculator.consolidateCallCount, 2) + assertEquals(calculator.calculateCallCount, 2) + } + + @Test + fun testProcessGrouping() { + val state = InternalMarketState() + val result = orderbookProcessor.processSubscribed( + existing = state, + tickSize = 0.1, + content = orderbookPayloadMock, + ) + assertEquals(calculator.consolidateCallCount, 1) + assertEquals(calculator.calculateCallCount, 1) + + orderbookProcessor.processGrouping( + existing = result, + tickSize = 0.1, + groupingMultiplier = 10, + ) + assertEquals(calculator.calculateCallCount, 2) + } +} diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/tests/mock/calculator/OrderbookCalculatorMock.kt b/src/commonTest/kotlin/exchange.dydx.abacus/tests/mock/calculator/OrderbookCalculatorMock.kt new file mode 100644 index 000000000..866ecef4a --- /dev/null +++ b/src/commonTest/kotlin/exchange.dydx.abacus/tests/mock/calculator/OrderbookCalculatorMock.kt @@ -0,0 +1,26 @@ +package exchange.dydx.abacus.tests.mock.calculator + +import exchange.dydx.abacus.calculator.OrderbookCalculatorProtocol +import exchange.dydx.abacus.output.MarketOrderbook +import exchange.dydx.abacus.state.internalstate.InternalOrderbook + +internal class OrderbookCalculatorMock : OrderbookCalculatorProtocol { + var calculateCallCount = 0 + var consolidateCallCount = 0 + var calculateAction: ((InternalOrderbook?, Double, Int) -> MarketOrderbook?)? = null + var consolidateAction: ((InternalOrderbook?) -> InternalOrderbook?)? = null + + override fun calculate( + rawOrderbook: InternalOrderbook?, + tickSize: Double, + groupingMultiplier: Int + ): MarketOrderbook? { + calculateCallCount++ + return calculateAction?.invoke(rawOrderbook, tickSize, groupingMultiplier) + } + + override fun consolidate(rawOrderbook: InternalOrderbook?): InternalOrderbook? { + consolidateCallCount++ + return consolidateAction?.invoke(rawOrderbook) + } +} From e8e56020e355adaa75abbc945ad5eb15aaadcb0f Mon Sep 17 00:00:00 2001 From: Rui Date: Tue, 6 Aug 2024 10:06:40 -0700 Subject: [PATCH 09/63] Updated codegen for candles --- .../kotlin/indexer/codegen/IndexerCandleResponseObject.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/commonMain/kotlin/indexer/codegen/IndexerCandleResponseObject.kt b/src/commonMain/kotlin/indexer/codegen/IndexerCandleResponseObject.kt index 5e1269125..d19f3d562 100644 --- a/src/commonMain/kotlin/indexer/codegen/IndexerCandleResponseObject.kt +++ b/src/commonMain/kotlin/indexer/codegen/IndexerCandleResponseObject.kt @@ -26,6 +26,8 @@ import kotlinx.serialization.Serializable * @param usdVolume * @param trades * @param startingOpenInterest + * @param orderbookMidPriceOpen + * @param orderbookMidPriceClose * @param id */ @Serializable @@ -42,5 +44,7 @@ data class IndexerCandleResponseObject( val usdVolume: kotlin.String? = null, val trades: kotlin.Double? = null, val startingOpenInterest: kotlin.String? = null, + val orderbookMidPriceOpen: kotlin.String? = null, + val orderbookMidPriceClose: kotlin.String? = null, val id: kotlin.String? = null ) From 4a6a15fbd650cf0d8bab8d2719f96c652ed9d10a Mon Sep 17 00:00:00 2001 From: Rui Date: Tue, 6 Aug 2024 15:31:14 -0700 Subject: [PATCH 10/63] Market Candles --- .../processor/markets/CandleProcessor.kt | 42 +++- .../processor/markets/CandlesProcessor.kt | 137 ++++++++--- .../markets/MarketsSummaryProcessor.kt | 50 +++- .../state/internalstate/InternalState.kt | 4 + .../model/TradingStateMachine+Candles.kt | 113 ++++++--- .../TradingStateMachine+HistoricalFunding.kt | 2 +- .../model/TradingStateMachine+Sparklines.kt | 26 +++ .../state/model/TradingStateMachine.kt | 26 ++- .../codegen/IndexerCandleResolution.kt | 32 ++- .../codegen/IndexerCandleResponseObject.kt | 4 - .../payload/v4/V4CandlesTests.kt | 220 ++++++++++++------ .../processor/markets/CandleProcessorTests.kt | 49 ++++ .../markets/CandlesProcessorTests.kt | 95 ++++++++ .../processor/markets/CandleProcessorMock.kt | 15 ++ swagger_codegen.sh | 25 ++ 15 files changed, 691 insertions(+), 149 deletions(-) create mode 100644 src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Sparklines.kt create mode 100644 src/commonTest/kotlin/exchange.dydx.abacus/processor/markets/CandleProcessorTests.kt create mode 100644 src/commonTest/kotlin/exchange.dydx.abacus/processor/markets/CandlesProcessorTests.kt create mode 100644 src/commonTest/kotlin/exchange.dydx.abacus/tests/mock/processor/markets/CandleProcessorMock.kt diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/CandleProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/CandleProcessor.kt index d5be05783..4cc6343a2 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/CandleProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/CandleProcessor.kt @@ -1,7 +1,10 @@ package exchange.dydx.abacus.processor.markets +import exchange.dydx.abacus.output.MarketCandle import exchange.dydx.abacus.processor.base.BaseProcessor import exchange.dydx.abacus.protocols.ParserProtocol +import exchange.dydx.abacus.utils.ParsingHelper.Companion.transform +import indexer.codegen.IndexerCandleResponseObject /* { @@ -32,7 +35,14 @@ import exchange.dydx.abacus.protocols.ParserProtocol "usdVolume": 226946.195 } */ -internal class CandleProcessor(parser: ParserProtocol) : BaseProcessor(parser) { + +internal interface CandleProcessorProtocol { + fun process(payload: IndexerCandleResponseObject?): MarketCandle? +} + +internal class CandleProcessor( + parser: ParserProtocol +) : BaseProcessor(parser), CandleProcessorProtocol { private val candleKeyMap = mapOf( "double" to mapOf( "low" to "low", @@ -52,6 +62,36 @@ internal class CandleProcessor(parser: ParserProtocol) : BaseProcessor(parser) { ), ) + override fun process( + payload: IndexerCandleResponseObject? + ): MarketCandle? { + if (payload == null) { + return null + } + val low = parser.asDouble(payload.low) + val high = parser.asDouble(payload.high) + val open = parser.asDouble(payload.open) + val close = parser.asDouble(payload.close) + val baseTokenVolume = parser.asDouble(payload.baseTokenVolume) + val usdVolume = parser.asDouble(payload.usdVolume) + val startedAtMilliseconds = parser.asDatetime(payload.startedAt)?.toEpochMilliseconds() + ?.toDouble() + if (low == null || high == null || open == null || close == null || baseTokenVolume == null || usdVolume == null || startedAtMilliseconds == null) { + return null + } + return MarketCandle( + startedAtMilliseconds = startedAtMilliseconds, + updatedAtMilliseconds = null, + low = low, + high = high, + open = open, + close = close, + baseTokenVolume = baseTokenVolume, + usdVolume = usdVolume, + trades = parser.asInt(payload.trades), + ) + } + override fun received( existing: Map?, payload: Map diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/CandlesProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/CandlesProcessor.kt index cadddc400..216c01a8e 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/CandlesProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/CandlesProcessor.kt @@ -2,12 +2,97 @@ package exchange.dydx.abacus.processor.markets import exchange.dydx.abacus.processor.base.BaseProcessor import exchange.dydx.abacus.protocols.ParserProtocol +import exchange.dydx.abacus.state.internalstate.InternalMarketState import exchange.dydx.abacus.utils.mutable import exchange.dydx.abacus.utils.safeSet +import indexer.codegen.IndexerCandleResponse +import indexer.codegen.IndexerCandleResponseObject +import kotlinx.datetime.Instant -@Suppress("UNCHECKED_CAST") -internal class CandlesProcessor(parser: ParserProtocol) : BaseProcessor(parser) { - private val itemProcessor = CandleProcessor(parser = parser) +internal class CandlesProcessor( + parser: ParserProtocol, + private val itemProcessor: CandleProcessorProtocol = CandleProcessor(parser = parser), +) : BaseProcessor(parser) { + + fun processSubscribed( + existing: InternalMarketState, + resolution: String, + payload: IndexerCandleResponse? + ): InternalMarketState { + if (payload != null && (payload.candles?.size ?: 0) > 0) { + val candles = payload.candles?.reversed()?.mapNotNull { + itemProcessor.process(it) + } + val merged = merge( + parser = parser, + existing = existing.candles?.get(resolution), + incoming = candles, + timeField = { + val startAt = it?.startedAtMilliseconds?.toLong() + if (startAt != null) Instant.fromEpochMilliseconds(startAt) else null + }, + ascending = true, + ) + if (merged != null) { + val modified = existing.candles ?: mutableMapOf() + modified[resolution] = merged + existing.candles = modified + } + } + return existing + } + + fun processBatchUpdate( + existing: InternalMarketState, + resolution: String, + payload: List? + ): InternalMarketState { + if (!payload.isNullOrEmpty()) { + val candles = payload.reversed().mapNotNull { + itemProcessor.process(it) + } + val merged = merge( + parser = parser, + existing = existing.candles?.get(resolution), + incoming = candles, + timeField = { + val startAt = it?.startedAtMilliseconds?.toLong() + if (startAt != null) Instant.fromEpochMilliseconds(startAt) else null + }, + ascending = true, + ) + if (merged != null) { + val modified = existing.candles ?: mutableMapOf() + modified[resolution] = merged + existing.candles = modified + } + } + return existing + } + + fun processUpdate( + existing: InternalMarketState, + resolution: String, + payload: IndexerCandleResponseObject? + ): InternalMarketState { + if (payload != null) { + val candle = itemProcessor.process(payload) + if (candle != null) { + val modified = existing.candles ?: mutableMapOf() + val existingResolution = modified[resolution]?.toMutableList() ?: mutableListOf() + val lastExisting = existingResolution.lastOrNull() + val lastStartAt = lastExisting?.startedAtMilliseconds + val incomingStartAt = candle.startedAtMilliseconds + if (lastStartAt == incomingStartAt) { + existingResolution.removeLast() + } + existingResolution.add(candle) + modified[resolution] = existingResolution + existing.candles = modified + } + } + return existing + } override fun received( existing: List?, @@ -43,7 +128,24 @@ internal class CandlesProcessor(parser: ParserProtocol) : BaseProcessor(parser) content: Map, ): Map? { // content is a single candle update - return receivedChange(existing, resolution, content) + if (content != null) { + val modified = existing?.mutable() ?: mutableMapOf() + val existingResolution = parser.asNativeList(existing?.get(resolution)) + val candles = existingResolution?.mutable() ?: mutableListOf() + val lastExisting = parser.asNativeMap(candles.lastOrNull()) + val lastStartAt = parser.asDatetime(lastExisting?.get("startedAt")) + val itemProcessor = itemProcessor as CandleProcessor + val incoming = itemProcessor.received(null, content) + val incomingStartAt = parser.asDatetime(incoming["startedAt"]) + if (lastStartAt == incomingStartAt) { + candles.removeLast() + } + candles.add(incoming) + modified.safeSet(resolution, candles) + return modified + } else { + return existing + } } private fun receivedChanges( @@ -57,7 +159,8 @@ internal class CandlesProcessor(parser: ParserProtocol) : BaseProcessor(parser) val candles = mutableListOf() for (value in payload.reversed()) { parser.asNativeMap(value)?.let { - val candle = itemProcessor.received(null, it) + val candleProcessor = itemProcessor as CandleProcessor + val candle = candleProcessor.received(null, it) candles.add(candle) } } @@ -74,28 +177,4 @@ internal class CandlesProcessor(parser: ParserProtocol) : BaseProcessor(parser) return existing } } - - private fun receivedChange( - existing: Map?, - resolution: String, - payload: Map, - ): Map? { - if (payload != null) { - val modified = existing?.mutable() ?: mutableMapOf() - val existingResolution = parser.asNativeList(existing?.get(resolution)) - val candles = existingResolution?.mutable() ?: mutableListOf() - val lastExisting = parser.asNativeMap(candles.lastOrNull()) - val lastStartAt = parser.asDatetime(lastExisting?.get("startedAt")) - val incoming = itemProcessor.received(null, payload) - val incomingStartAt = parser.asDatetime(incoming["startedAt"]) - if (lastStartAt == incomingStartAt) { - candles.removeLast() - } - candles.add(incoming) - modified.safeSet(resolution, candles) - return modified - } else { - return existing - } - } } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsSummaryProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsSummaryProcessor.kt index fec15333d..3ea147606 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsSummaryProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsSummaryProcessor.kt @@ -6,6 +6,8 @@ import exchange.dydx.abacus.state.internalstate.InternalMarketState import exchange.dydx.abacus.state.internalstate.InternalMarketSummaryState import exchange.dydx.abacus.utils.mutable import exchange.dydx.abacus.utils.safeSet +import indexer.codegen.IndexerCandleResponse +import indexer.codegen.IndexerCandleResponseObject import indexer.codegen.IndexerOrderbookResponseObject import indexer.models.IndexerCompositeMarketObject import indexer.models.IndexerWsMarketUpdateResponse @@ -18,6 +20,7 @@ internal class MarketsSummaryProcessor( ) : BaseProcessor(parser) { private val marketsProcessor = MarketsProcessor(parser, calculateSparklines) private val orderbookProcessor = OrderbookProcessor(parser) + private val candlesProcessor = CandlesProcessor(parser) internal var groupingMultiplier: Int get() = if (staticTyping) orderbookProcessor.groupingMultiplier else marketsProcessor.groupingMultiplier @@ -58,6 +61,51 @@ internal class MarketsSummaryProcessor( return existing } + fun processCandles( + existing: InternalMarketSummaryState, + marketId: String, + resolution: String, + content: IndexerCandleResponse? + ): InternalMarketSummaryState { + val marketState = existing.markets[marketId] ?: InternalMarketState() + existing.markets[marketId] = candlesProcessor.processSubscribed( + existing = marketState, + resolution = resolution, + payload = content, + ) + return existing + } + + fun processBatchCandlesChanges( + existing: InternalMarketSummaryState, + marketId: String, + resolution: String, + content: List?, + ): InternalMarketSummaryState { + val marketState = existing.markets[marketId] ?: InternalMarketState() + existing.markets[marketId] = candlesProcessor.processBatchUpdate( + existing = marketState, + resolution = resolution, + payload = content, + ) + return existing + } + + fun processCandlesChanges( + existing: InternalMarketSummaryState, + marketId: String, + resolution: String, + content: IndexerCandleResponseObject?, + ): InternalMarketSummaryState { + val marketState = existing.markets[marketId] ?: InternalMarketState() + existing.markets[marketId] = candlesProcessor.processUpdate( + existing = marketState, + resolution = resolution, + payload = content, + ) + return existing + } + fun processOrderbook( existing: InternalMarketSummaryState, tickSize: Double?, @@ -221,7 +269,7 @@ internal class MarketsSummaryProcessor( return modify(existing, markets) } - internal fun receivedCandles( + internal fun receivedCandlesDeprecated( existing: Map?, market: String, resolution: String, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt index b6a7bd18e..720b6bce4 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt @@ -7,6 +7,7 @@ import exchange.dydx.abacus.output.EquityTiers import exchange.dydx.abacus.output.FeeTier import exchange.dydx.abacus.output.LaunchIncentivePoint import exchange.dydx.abacus.output.LaunchIncentiveSeason +import exchange.dydx.abacus.output.MarketCandle import exchange.dydx.abacus.output.MarketOrderbook import exchange.dydx.abacus.output.MarketTrade import exchange.dydx.abacus.output.PerpetualMarket @@ -57,6 +58,9 @@ internal data class InternalMarketState( // grouped orderbook for app display var groupedOrderbook: MarketOrderbook? = null, + + // candles: resolution -> candles + var candles: MutableMap>? = null ) internal data class InternalOrderbook( diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Candles.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Candles.kt index 40b4bac28..4c0b393dc 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Candles.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Candles.kt @@ -1,11 +1,16 @@ package exchange.dydx.abacus.state.model -import exchange.dydx.abacus.protocols.asTypedStringMapOfList +import exchange.dydx.abacus.protocols.asTypedList +import exchange.dydx.abacus.protocols.asTypedObject import exchange.dydx.abacus.state.changes.Changes import exchange.dydx.abacus.state.changes.StateChanges +import indexer.codegen.IndexerCandleResponse +import indexer.codegen.IndexerCandleResponseObject import kollections.iListOf import kollections.toIList +import kotlinx.serialization.json.JsonNull.content +// Called in test code only internal fun TradingStateMachine.candles(payload: String): StateChanges { val json = parser.decodeJsonObject(payload) return if (json != null) { @@ -25,14 +30,47 @@ private fun TradingStateMachine.receivedCandles(payload: Map): Stat if (marketId != null) iListOf(marketId) else null } return if (marketIds != null) { - val size = parser.asList(payload["candles"])?.size ?: 0 + val list = parser.asList(payload["candles"]) + val size = list?.size ?: 0 if (size > 0) { - marketsSummary = marketsProcessor.receivedCandles(marketsSummary, payload) + if (staticTyping) { + for (marketId in marketIds) { + val candlesPayload = + parser.asTypedList(list) + val resolution = candlesPayload?.firstOrNull()?.resolution + if (resolution != null) { + marketsProcessor.processBatchCandlesChanges( + existing = internalState.marketsSummary, + marketId = marketId, + resolution = resolution.value, + content = candlesPayload, + ) + } + } + } else { + marketsSummary = marketsProcessor.receivedCandles(marketsSummary, payload) + } StateChanges(iListOf(Changes.candles), marketIds) } else { val size = parser.asMap(payload["candles"])?.size ?: 0 if (size > 0) { - marketsSummary = marketsProcessor.receivedCandles(marketsSummary, payload) + if (staticTyping) { + for (marketId in marketIds) { + val candlesPayload = + parser.asTypedList(markets?.get(marketId)) + val resolution = candlesPayload?.firstOrNull()?.resolution + if (resolution != null) { + marketsProcessor.processBatchCandlesChanges( + existing = internalState.marketsSummary, + marketId = marketId, + resolution = resolution.value, + content = candlesPayload, + ) + } + } + } else { + marketsSummary = marketsProcessor.receivedCandles(marketsSummary, payload) + } StateChanges(iListOf(Changes.candles), marketIds) } else { StateChanges(iListOf()) @@ -43,34 +81,25 @@ private fun TradingStateMachine.receivedCandles(payload: Map): Stat } } -internal fun TradingStateMachine.sparklines(payload: String): StateChanges? { - val json = parser.decodeJsonObject(payload) as? Map> - if (staticTyping) { - val sparklines = parser.asTypedStringMapOfList(json) - return if (sparklines != null) { - marketsProcessor.processSparklines(internalState.marketsSummary, sparklines) - StateChanges(iListOf(Changes.sparklines, Changes.markets), null) - } else { - StateChanges.noChange - } - } else { - return if (json != null) { - marketsSummary = marketsProcessor.receivedSparklinesDeprecated(marketsSummary, json) - return StateChanges(iListOf(Changes.sparklines, Changes.markets), null) - } else { - StateChanges.noChange - } - } -} - internal fun TradingStateMachine.receivedCandles( - market: String, + marketId: String, resolution: String, payload: Map ): StateChanges { - this.marketsSummary = - marketsProcessor.receivedCandles(marketsSummary, market, resolution, payload) - return StateChanges(iListOf(Changes.candles), iListOf(market)) + if (staticTyping) { + val candlesPayload = parser.asTypedObject(payload) + print(candlesPayload) + marketsProcessor.processCandles( + existing = internalState.marketsSummary, + marketId = marketId, + resolution = resolution, + content = candlesPayload, + ) + } else { + this.marketsSummary = + marketsProcessor.receivedCandlesDeprecated(marketsSummary, marketId, resolution, payload) + } + return StateChanges(iListOf(Changes.candles), iListOf(marketId)) } internal fun TradingStateMachine.receivedCandlesChanges( @@ -78,8 +107,18 @@ internal fun TradingStateMachine.receivedCandlesChanges( resolution: String, payload: Map ): StateChanges { - this.marketsSummary = - marketsProcessor.receivedCandlesChanges(marketsSummary, market, resolution, payload) + if (staticTyping) { + val candlesPayload = parser.asTypedObject(payload) + marketsProcessor.processCandlesChanges( + existing = internalState.marketsSummary, + marketId = market, + resolution = resolution, + content = candlesPayload, + ) + } else { + this.marketsSummary = + marketsProcessor.receivedCandlesChanges(marketsSummary, market, resolution, payload) + } return StateChanges(iListOf(Changes.candles), iListOf(market)) } @@ -88,7 +127,17 @@ internal fun TradingStateMachine.receivedBatchedCandlesChanges( resolution: String, payload: List ): StateChanges { - this.marketsSummary = - marketsProcessor.receivedBatchedCandlesChanges(marketsSummary, market, resolution, payload) + if (staticTyping) { + val candlesPayload = parser.asTypedList(payload) + marketsProcessor.processBatchCandlesChanges( + existing = internalState.marketsSummary, + marketId = market, + resolution = resolution, + content = candlesPayload, + ) + } else { + this.marketsSummary = + marketsProcessor.receivedBatchedCandlesChanges(marketsSummary, market, resolution, payload) + } return StateChanges(iListOf(Changes.candles), iListOf(market)) } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+HistoricalFunding.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+HistoricalFunding.kt index fc2862bae..45b2bca2f 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+HistoricalFunding.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+HistoricalFunding.kt @@ -13,7 +13,7 @@ internal fun TradingStateMachine.historicalFundings(payload: String): StateChang } } -internal fun TradingStateMachine.receivedHistoricalFundings(payload: Map): StateChanges { +private fun TradingStateMachine.receivedHistoricalFundings(payload: Map): StateChanges { val marketId = parser.asString( parser.value(payload, "historicalFunding.0.market") ?: parser.value( payload, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Sparklines.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Sparklines.kt new file mode 100644 index 000000000..b70012455 --- /dev/null +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Sparklines.kt @@ -0,0 +1,26 @@ +package exchange.dydx.abacus.state.model + +import exchange.dydx.abacus.protocols.asTypedStringMapOfList +import exchange.dydx.abacus.state.changes.Changes +import exchange.dydx.abacus.state.changes.StateChanges +import kollections.iListOf + +internal fun TradingStateMachine.sparklines(payload: String): StateChanges? { + val json = parser.decodeJsonObject(payload) as? Map> + if (staticTyping) { + val sparklines = parser.asTypedStringMapOfList(json) + return if (sparklines != null) { + marketsProcessor.processSparklines(internalState.marketsSummary, sparklines) + StateChanges(iListOf(Changes.sparklines, Changes.markets), null) + } else { + StateChanges.noChange + } + } else { + return if (json != null) { + marketsSummary = marketsProcessor.receivedSparklinesDeprecated(marketsSummary, json) + return StateChanges(iListOf(Changes.sparklines, Changes.markets), null) + } else { + StateChanges.noChange + } + } +} diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt index b5d737037..1b2fa40e1 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt @@ -13,6 +13,7 @@ import exchange.dydx.abacus.output.Asset import exchange.dydx.abacus.output.Configs import exchange.dydx.abacus.output.LaunchIncentive import exchange.dydx.abacus.output.LaunchIncentiveSeasons +import exchange.dydx.abacus.output.MarketCandle import exchange.dydx.abacus.output.MarketCandles import exchange.dydx.abacus.output.MarketHistoricalFunding import exchange.dydx.abacus.output.MarketOrderbook @@ -77,6 +78,7 @@ import kollections.iListOf import kollections.iMutableListOf import kollections.iMutableMapOf import kollections.toIList +import kollections.toIMap import kollections.toIMutableMap import kotlinx.serialization.SerializationException import kotlinx.serialization.json.Json @@ -1180,11 +1182,25 @@ open class TradingStateMachine( if (markets != null) { val modified = candles?.toIMutableMap() ?: mutableMapOf() for (marketId in markets) { - val data = - parser.asNativeMap(parser.value(data, "markets.markets.$marketId.candles")) - val existing = candles?.get(marketId) - val candles = MarketCandles.create(existing, parser, data) - modified.typedSafeSet(marketId, candles) + if (staticTyping) { + val candles = internalState.marketsSummary.markets[marketId]?.candles + val marketCandles: MutableMap> = mutableMapOf() + for ((key, value) in candles ?: emptyMap()) { + marketCandles[key] = value.toIList() + } + modified.typedSafeSet(marketId, MarketCandles(candles = marketCandles.toIMap())) + } else { + val data = + parser.asNativeMap( + parser.value( + data, + "markets.markets.$marketId.candles", + ), + ) + val existing = candles?.get(marketId) + val candles = MarketCandles.create(existing, parser, data) + modified.typedSafeSet(marketId, candles) + } } candles = modified } else { diff --git a/src/commonMain/kotlin/indexer/codegen/IndexerCandleResolution.kt b/src/commonMain/kotlin/indexer/codegen/IndexerCandleResolution.kt index edd4e32e3..d40f00a84 100644 --- a/src/commonMain/kotlin/indexer/codegen/IndexerCandleResolution.kt +++ b/src/commonMain/kotlin/indexer/codegen/IndexerCandleResolution.kt @@ -11,6 +11,7 @@ */ package indexer.codegen +import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable /** @@ -19,11 +20,30 @@ import kotlinx.serialization.Serializable */ @Serializable enum class IndexerCandleResolution(val value: kotlin.String) { - _1MIN("1MIN"), // :/ - _5MINS("5MINS"), // :/ - _15MINS("15MINS"), // :/ - _30MINS("30MINS"), // :/ - _1HOUR("1HOUR"), // :/ - _4HOURS("4HOURS"), // :/ + @SerialName("1MIN") + _1MIN("1MIN"), + + // :/ + @SerialName("5MINS") + _5MINS("5MINS"), + + // :/ + @SerialName("15MINS") + _15MINS("15MINS"), + + // :/ + @SerialName("30MINS") + _30MINS("30MINS"), + + // :/ + @SerialName("1HOUR") + _1HOUR("1HOUR"), + + // :/ + @SerialName("4HOURS") + _4HOURS("4HOURS"), + + // :/ + @SerialName("1DAY") _1DAY("1DAY"); // :/ } diff --git a/src/commonMain/kotlin/indexer/codegen/IndexerCandleResponseObject.kt b/src/commonMain/kotlin/indexer/codegen/IndexerCandleResponseObject.kt index d19f3d562..5e1269125 100644 --- a/src/commonMain/kotlin/indexer/codegen/IndexerCandleResponseObject.kt +++ b/src/commonMain/kotlin/indexer/codegen/IndexerCandleResponseObject.kt @@ -26,8 +26,6 @@ import kotlinx.serialization.Serializable * @param usdVolume * @param trades * @param startingOpenInterest - * @param orderbookMidPriceOpen - * @param orderbookMidPriceClose * @param id */ @Serializable @@ -44,7 +42,5 @@ data class IndexerCandleResponseObject( val usdVolume: kotlin.String? = null, val trades: kotlin.Double? = null, val startingOpenInterest: kotlin.String? = null, - val orderbookMidPriceOpen: kotlin.String? = null, - val orderbookMidPriceClose: kotlin.String? = null, val id: kotlin.String? = null ) diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4CandlesTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4CandlesTests.kt index 0f432d63a..ac409fbe2 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4CandlesTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4CandlesTests.kt @@ -45,11 +45,19 @@ class V4CandlesTests : V3BaseTests() { } private fun testCandlesAllMarkets() { - test( - { - perp.loadCandlesAllMarkets(mock) - }, - """ + if (perp.staticTyping) { + perp.loadCandlesAllMarkets(mock) + val market = perp.internalState.marketsSummary.markets["ETH-USD"] + assertEquals(1, market?.candles?.size) + val firstItem = market?.candles?.get("1HOUR")?.first() + assertEquals(1785.7, firstItem?.open) + assertEquals(1797.4, firstItem?.close) + } else { + test( + { + perp.loadCandlesAllMarkets(mock) + }, + """ { "markets": { "markets": { @@ -72,16 +80,24 @@ class V4CandlesTests : V3BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } private fun testCandlesFirstCall() { - test( - { - perp.loadCandlesFirst(mock) - }, - """ + if (perp.staticTyping) { + perp.loadCandlesFirst(mock) + val market = perp.internalState.marketsSummary.markets["ETH-USD"] + val firstItem = market?.candles?.get("15MINS")?.first() + assertEquals(1780.6, firstItem?.open) + assertEquals(1782.3, firstItem?.close) + } else { + test( + { + perp.loadCandlesFirst(mock) + }, + """ { "markets": { "markets": { @@ -104,16 +120,24 @@ class V4CandlesTests : V3BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } private fun testCandlesSecondCall() { - test( - { - perp.loadCandlesSecond(mock) - }, - """ + if (perp.staticTyping) { + perp.loadCandlesSecond(mock) + val market = perp.internalState.marketsSummary.markets["ETH-USD"] + val firstItem = market?.candles?.get("15MINS")?.first() + assertEquals(1709.7, firstItem?.open) + assertEquals(1709.7, firstItem?.close) + } else { + test( + { + perp.loadCandlesSecond(mock) + }, + """ { "markets": { "markets": { @@ -136,16 +160,24 @@ class V4CandlesTests : V3BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } private fun testCandlesSubscribed() { - test( - { - perp.socket(testWsUrl, mock.candles.v4_subscribed, 0, null) - }, - """ + if (perp.staticTyping) { + perp.socket(testWsUrl, mock.candles.v4_subscribed, 0, null) + val market = perp.internalState.marketsSummary.markets["ETH-USD"] + val firstItem = market?.candles?.get("1HOUR")?.first() + assertEquals(1785.7, firstItem?.open) + assertEquals(1797.4, firstItem?.close) + } else { + test( + { + perp.socket(testWsUrl, mock.candles.v4_subscribed, 0, null) + }, + """ { "markets": { "markets": { @@ -168,19 +200,36 @@ class V4CandlesTests : V3BaseTests() { } } } - """.trimIndent(), - { response -> - assertEquals(125, response.state?.candles?.get("ETH-USD")?.candles?.get("1HOUR")?.size) - }, - ) + """.trimIndent(), + { response -> + assertEquals( + 125, + response.state?.candles?.get("ETH-USD")?.candles?.get("1HOUR")?.size, + ) + }, + ) + } } private fun testCandlesChannelData() { - test( - { - perp.socket(testWsUrl, mock.candles.v4_channel_data, 0, null) - }, - """ + if (perp.staticTyping) { + perp.socket(testWsUrl, mock.candles.v4_channel_data, 0, null) + val market = perp.internalState.marketsSummary.markets["ETH-USD"] + val firstItem = market?.candles?.get("15MINS")?.first() + assertEquals(1709.7, firstItem?.open) + assertEquals(1709.7, firstItem?.close) + + val candles = perp.internalState.marketsSummary.markets["ETH-USD"]?.candles?.get("1HOUR") + assertEquals(125, candles?.size) + val lastCandle = candles?.last() + assertEquals(1582.8, lastCandle?.close) + assertEquals(1577.7, lastCandle?.open) + } else { + test( + { + perp.socket(testWsUrl, mock.candles.v4_channel_data, 0, null) + }, + """ { "markets": { "markets": { @@ -203,22 +252,35 @@ class V4CandlesTests : V3BaseTests() { } } } - """.trimIndent(), - { response -> - assertEquals(125, response.state?.candles?.get("ETH-USD")?.candles?.get("1HOUR")?.size) - val lastCandle = response.state?.candles?.get("ETH-USD")?.candles?.get("1HOUR")?.last() - assertEquals(1582.8, lastCandle?.close) - assertEquals(1577.7, lastCandle?.open) - }, - ) + """.trimIndent(), + { response -> + assertEquals( + 125, + response.state?.candles?.get("ETH-USD")?.candles?.get("1HOUR")?.size, + ) + val lastCandle = + response.state?.candles?.get("ETH-USD")?.candles?.get("1HOUR")?.last() + assertEquals(1582.8, lastCandle?.close) + assertEquals(1577.7, lastCandle?.open) + }, + ) + } } private fun testCandlesChannelBatchData() { - test( - { - perp.socket(testWsUrl, mock.candles.v4_channel_batch_data, 0, null) - }, - """ + if (perp.staticTyping) { + perp.socket(testWsUrl, mock.candles.v4_channel_batch_data, 0, null) + val candles = perp.state?.candles?.get("ETH-USD")?.candles?.get("1HOUR") + assertEquals(126, candles?.size) + val lastCandle = candles?.last() + assertEquals(1590.8, lastCandle?.close) + assertEquals(1598.0, lastCandle?.open) + } else { + test( + { + perp.socket(testWsUrl, mock.candles.v4_channel_batch_data, 0, null) + }, + """ { "markets": { "markets": { @@ -233,20 +295,33 @@ class V4CandlesTests : V3BaseTests() { } } } - """.trimIndent(), - { response -> - assertEquals(126, response.state?.candles?.get("ETH-USD")?.candles?.get("1HOUR")?.size) - val lastCandle = response.state?.candles?.get("ETH-USD")?.candles?.get("1HOUR")?.last() - assertEquals(1590.8, lastCandle?.close) - assertEquals(1598.0, lastCandle?.open) - }, - ) + """.trimIndent(), + { response -> + assertEquals( + 126, + response.state?.candles?.get("ETH-USD")?.candles?.get("1HOUR")?.size, + ) + val lastCandle = + response.state?.candles?.get("ETH-USD")?.candles?.get("1HOUR")?.last() + assertEquals(1590.8, lastCandle?.close) + assertEquals(1598.0, lastCandle?.open) + }, + ) + } - test( - { - perp.socket(testWsUrl, mock.candles.v4_channel_batch_data_2, 0, null) - }, - """ + if (perp.staticTyping) { + perp.socket(testWsUrl, mock.candles.v4_channel_batch_data_2, 0, null) + val candles = perp.state?.candles?.get("ETH-USD")?.candles?.get("1HOUR") + assertEquals(126, candles?.size) + val lastCandle = candles?.last() + assertEquals(1592.7, lastCandle?.close) + assertEquals(1598.0, lastCandle?.open) + } else { + test( + { + perp.socket(testWsUrl, mock.candles.v4_channel_batch_data_2, 0, null) + }, + """ { "markets": { "markets": { @@ -261,13 +336,18 @@ class V4CandlesTests : V3BaseTests() { } } } - """.trimIndent(), - { response -> - assertEquals(126, response.state?.candles?.get("ETH-USD")?.candles?.get("1HOUR")?.size) - val lastCandle = response.state?.candles?.get("ETH-USD")?.candles?.get("1HOUR")?.last() - assertEquals(1592.7, lastCandle?.close) - assertEquals(1598.0, lastCandle?.open) - }, - ) + """.trimIndent(), + { response -> + assertEquals( + 126, + response.state?.candles?.get("ETH-USD")?.candles?.get("1HOUR")?.size, + ) + val lastCandle = + response.state?.candles?.get("ETH-USD")?.candles?.get("1HOUR")?.last() + assertEquals(1592.7, lastCandle?.close) + assertEquals(1598.0, lastCandle?.open) + }, + ) + } } } diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/processor/markets/CandleProcessorTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/processor/markets/CandleProcessorTests.kt new file mode 100644 index 000000000..c50f81332 --- /dev/null +++ b/src/commonTest/kotlin/exchange.dydx.abacus/processor/markets/CandleProcessorTests.kt @@ -0,0 +1,49 @@ +package exchange.dydx.abacus.processor.markets + +import exchange.dydx.abacus.output.MarketCandle +import exchange.dydx.abacus.utils.Parser +import indexer.codegen.IndexerCandleResolution +import indexer.codegen.IndexerCandleResponseObject +import kotlinx.datetime.Instant +import kotlin.test.Test +import kotlin.test.assertEquals + +class CandleProcessorTests { + companion object { + val startedAt = Instant.parse("2022-08-09T20:00:00.000Z") + val candlePayloadMock = IndexerCandleResponseObject( + high = "0.91", + low = "0.6", + startedAt = startedAt.toString(), + ticker = null, + resolution = IndexerCandleResolution._1HOUR, + open = "0.8", + close = "0.9", + baseTokenVolume = "200", + usdVolume = "180", + trades = 10.0, + startingOpenInterest = "1000", + id = null, + ) + + val resultMock = MarketCandle( + startedAtMilliseconds = startedAt.toEpochMilliseconds().toDouble(), + updatedAtMilliseconds = null, + low = 0.6, + high = 0.91, + open = 0.8, + close = 0.9, + baseTokenVolume = 200.0, + usdVolume = 180.0, + trades = 10, + ) + } + + private val processor = CandleProcessor(parser = Parser()) + + @Test + fun testProcess() { + val result = processor.process(candlePayloadMock) + assertEquals(resultMock, result) + } +} diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/processor/markets/CandlesProcessorTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/processor/markets/CandlesProcessorTests.kt new file mode 100644 index 000000000..fda5b0df8 --- /dev/null +++ b/src/commonTest/kotlin/exchange.dydx.abacus/processor/markets/CandlesProcessorTests.kt @@ -0,0 +1,95 @@ +package exchange.dydx.abacus.processor.markets + +import exchange.dydx.abacus.state.internalstate.InternalMarketState +import exchange.dydx.abacus.tests.mock.processor.markets.CandleProcessorMock +import exchange.dydx.abacus.utils.Parser +import indexer.codegen.IndexerCandleResponse +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.time.Duration.Companion.hours + +class CandlesProcessorTests { + + private val itemProcessor = CandleProcessorMock() + private val processor = CandlesProcessor(parser = Parser(), itemProcessor = itemProcessor) + + @Test + fun testProcessSubscribed() { + // Given + val existing = InternalMarketState() + val resolution = "1HOUR" + val payload = IndexerCandleResponse( + candles = arrayOf(CandleProcessorTests.candlePayloadMock), + ) + itemProcessor.processAction = { it?.let { CandleProcessorTests.resultMock } } + + // When + val result = processor.processSubscribed(existing, resolution, payload) + + // Then + val expected = mapOf( + resolution to listOf(CandleProcessorTests.resultMock), + ) + assertEquals(expected, result.candles?.toMap()) + } + + @Test + fun testProcessBatchUpdate() { + // Given + val existing = InternalMarketState() + val resolution = "1HOUR" + val payload = IndexerCandleResponse( + candles = arrayOf(CandleProcessorTests.candlePayloadMock), + ) + itemProcessor.processAction = { it?.let { CandleProcessorTests.resultMock } } + + var result = processor.processSubscribed(existing, resolution, payload) + assertEquals(result.candles?.get(resolution)?.size, 1) + + val updatePayload = listOf(CandleProcessorTests.candlePayloadMock) + itemProcessor.processAction = { + it?.let { + CandleProcessorTests.resultMock.copy( + startedAtMilliseconds = CandleProcessorTests.startedAt.plus(2.hours) + .toEpochMilliseconds().toDouble(), + ) + } + } + + // When + result = processor.processBatchUpdate(result, resolution, updatePayload) + + // Then + assertEquals(result.candles?.get(resolution)?.size, 2) + } + + @Test + fun testProcessUpdate() { + // Given + val existing = InternalMarketState() + val resolution = "1HOUR" + val payload = IndexerCandleResponse( + candles = arrayOf(CandleProcessorTests.candlePayloadMock), + ) + itemProcessor.processAction = { it?.let { CandleProcessorTests.resultMock } } + + var result = processor.processSubscribed(existing, resolution, payload) + assertEquals(result.candles?.get(resolution)?.size, 1) + + val updatePayload = CandleProcessorTests.candlePayloadMock + itemProcessor.processAction = { + it?.let { + CandleProcessorTests.resultMock.copy( + startedAtMilliseconds = CandleProcessorTests.startedAt.plus(2.hours) + .toEpochMilliseconds().toDouble(), + ) + } + } + + // When + result = processor.processUpdate(result, resolution, updatePayload) + + // Then + assertEquals(result.candles?.get(resolution)?.size, 2) + } +} diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/tests/mock/processor/markets/CandleProcessorMock.kt b/src/commonTest/kotlin/exchange.dydx.abacus/tests/mock/processor/markets/CandleProcessorMock.kt new file mode 100644 index 000000000..485cd760d --- /dev/null +++ b/src/commonTest/kotlin/exchange.dydx.abacus/tests/mock/processor/markets/CandleProcessorMock.kt @@ -0,0 +1,15 @@ +package exchange.dydx.abacus.tests.mock.processor.markets + +import exchange.dydx.abacus.output.MarketCandle +import exchange.dydx.abacus.processor.markets.CandleProcessorProtocol +import indexer.codegen.IndexerCandleResponseObject + +class CandleProcessorMock : CandleProcessorProtocol { + var processCallCount = 0 + var processAction: ((IndexerCandleResponseObject?) -> MarketCandle?)? = null + + override fun process(payload: IndexerCandleResponseObject?): MarketCandle? { + processCallCount++ + return processAction?.invoke(payload) + } +} diff --git a/swagger_codegen.sh b/swagger_codegen.sh index 7c7569dc5..4fed07522 100755 --- a/swagger_codegen.sh +++ b/swagger_codegen.sh @@ -74,6 +74,31 @@ sed -i '' 's/\ PerpetualPositionsMap/\ Map in SubaccountResponseObject.kt sed -i '' 's/\ AssetPositionsMap/\ Map/g' generated/src/main/kotlin/indexer/codegen/SubaccountResponseObject.kt +# add import kotlinx.serialization.Serializable to the top of CandleResolution.kt +sed -i '' 's/package indexer.codegen/package indexer.codegen\n\nimport kotlinx.serialization.Serializable/' generated/src/main/kotlin/indexer/codegen/CandleResolution.kt + +# add @SerialName("1MIN") to _1MIN("1MIN") in CandleResolution.kt +sed -i '' 's/_1MIN("1MIN")/@SerialName("1MIN")\n _1MIN("1MIN")/' generated/src/main/kotlin/indexer/codegen/CandleResolution.kt + +# add @SerialName("5MIN") to _5MIN("5MIN") in CandleResolution.kt +sed -i '' 's/_5MINS("5MINS")/@SerialName("5MINS")\n _5MINS("5MINS")/' generated/src/main/kotlin/indexer/codegen/CandleResolution.kt + +# add @SerialName("15MIN") to _15MIN("15MIN") in CandleResolution.kt +sed -i '' 's/_15MINS("15MINS")/@SerialName("15MINS")\n _15MINS("15MINS")/' generated/src/main/kotlin/indexer/codegen/CandleResolution.kt + +# add @SerialName("30MIN") to _30MIN("30MIN") in CandleResolution.kt +sed -i '' 's/_30MINS("30MINS")/@SerialName("30MINS")\n _30MINS("30MINS")/' generated/src/main/kotlin/indexer/codegen/CandleResolution.kt + +# add @SerialName("1HOUR") to _1HOUR("1HOUR") in CandleResolution.kt +sed -i '' 's/_1HOUR("1HOUR")/@SerialName("1HOUR")\n _1HOUR("1HOUR")/' generated/src/main/kotlin/indexer/codegen/CandleResolution.kt + +# add @SerialName("4HOUR") to _4HOUR("4HOUR") in CandleResolution.kt +sed -i '' 's/_4HOURS("4HOURS")/@SerialName("4HOURS")\n _4HOURS("4HOURS")/' generated/src/main/kotlin/indexer/codegen/CandleResolution.kt + +# add @SerialName("1DAY") to _1DAY("1DAY") in CandleResolution.kt +sed -i '' 's/_1DAY("1DAY")/@SerialName("1DAY")\n _1DAY("1DAY")/' generated/src/main/kotlin/indexer/codegen/CandleResolution.kt + + # for each of the time in the generated code, run "swagger_update_file.sh " find generated/src/main/kotlin/indexer -type f \ -exec $CURRENT_DIR/swagger_update_file.sh {} \; From 462a8ff91f232f368443c8591a4f914a887207be Mon Sep 17 00:00:00 2001 From: mobile-build-bot-git Date: Tue, 6 Aug 2024 22:33:47 +0000 Subject: [PATCH 11/63] Bump version --- build.gradle.kts | 2 +- v4_abacus.podspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 71ee6457e..a030a5024 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -52,7 +52,7 @@ allprojects { } group = "exchange.dydx.abacus" -version = "1.8.77" +version = "1.8.78" repositories { google() diff --git a/v4_abacus.podspec b/v4_abacus.podspec index d61c9d1fd..44c131e45 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.77' + spec.version = '1.8.78' spec.homepage = 'https://github.com/dydxprotocol/v4-abacus' spec.source = { :http=> ''} spec.authors = '' From d52adce850f2beb325d7b11c436f90ff63cfaf9a Mon Sep 17 00:00:00 2001 From: Rui Date: Tue, 6 Aug 2024 18:27:58 -0700 Subject: [PATCH 12/63] Add market trades to the output state. --- .../markets/MarketsSummaryProcessor.kt | 30 ++++ ...adeProcessorV2.kt => TradesProcessorV2.kt} | 38 +++-- .../model/TradingStateMachine+Candles.kt | 1 - .../state/model/TradingStateMachine+Trades.kt | 58 +++---- .../state/model/TradingStateMachine.kt | 27 +-- .../exchange.dydx.abacus/payload/BaseTests.kt | 16 +- .../payload/v3/V3MarketsDelayedTests.kt | 111 ++++++++----- .../v4/V4DuplicateWebsocketMessageTests.kt | 25 ++- .../payload/v4/V4MarketsTests.kt | 156 +++++++++++------- .../markets/TradesProcessorV2Tests.kt | 65 ++++---- 10 files changed, 324 insertions(+), 203 deletions(-) rename src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/{TradeProcessorV2.kt => TradesProcessorV2.kt} (74%) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsSummaryProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsSummaryProcessor.kt index 3ea147606..ebf000acf 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsSummaryProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsSummaryProcessor.kt @@ -1,6 +1,7 @@ package exchange.dydx.abacus.processor.markets import exchange.dydx.abacus.processor.base.BaseProcessor +import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.internalstate.InternalMarketState import exchange.dydx.abacus.state.internalstate.InternalMarketSummaryState @@ -9,18 +10,21 @@ import exchange.dydx.abacus.utils.safeSet import indexer.codegen.IndexerCandleResponse import indexer.codegen.IndexerCandleResponseObject import indexer.codegen.IndexerOrderbookResponseObject +import indexer.codegen.IndexerTradeResponse import indexer.models.IndexerCompositeMarketObject import indexer.models.IndexerWsMarketUpdateResponse import indexer.models.IndexerWsOrderbookUpdateResponse internal class MarketsSummaryProcessor( parser: ParserProtocol, + localizer: LocalizerProtocol?, calculateSparklines: Boolean = false, private val staticTyping: Boolean, ) : BaseProcessor(parser) { private val marketsProcessor = MarketsProcessor(parser, calculateSparklines) private val orderbookProcessor = OrderbookProcessor(parser) private val candlesProcessor = CandlesProcessor(parser) + private val tradesProcessor = TradesProcessorV2(TradeProcessorV2(parser, localizer)) internal var groupingMultiplier: Int get() = if (staticTyping) orderbookProcessor.groupingMultiplier else marketsProcessor.groupingMultiplier @@ -151,6 +155,32 @@ internal class MarketsSummaryProcessor( return existing } + fun processTradesSubscribed( + existing: InternalMarketSummaryState, + marketId: String, + content: IndexerTradeResponse? + ): InternalMarketSummaryState { + val marketState = existing.markets[marketId] ?: InternalMarketState() + existing.markets[marketId] = tradesProcessor.processSubscribed( + existing = marketState, + payload = content, + ) + return existing + } + + fun processTradesUpdates( + existing: InternalMarketSummaryState, + marketId: String, + content: IndexerTradeResponse? + ): InternalMarketSummaryState { + val marketState = existing.markets[marketId] ?: InternalMarketState() + existing.markets[marketId] = tradesProcessor.processChannelData( + existing = marketState, + payload = content, + ) + return existing + } + internal fun subscribedDeprecated( existing: Map?, content: Map diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/TradeProcessorV2.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/TradesProcessorV2.kt similarity index 74% rename from src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/TradeProcessorV2.kt rename to src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/TradesProcessorV2.kt index 338bd0de4..6dd42cd54 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/TradeProcessorV2.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/TradesProcessorV2.kt @@ -7,45 +7,49 @@ import exchange.dydx.abacus.output.input.OrderType import exchange.dydx.abacus.processor.base.mergeWithIds import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol +import exchange.dydx.abacus.state.internalstate.InternalMarketState import exchange.dydx.abacus.utils.IndexerResponseParsingException import exchange.dydx.abacus.utils.Logger import exchange.dydx.abacus.utils.parseException +import indexer.codegen.IndexerTradeResponse import indexer.codegen.IndexerTradeResponseObject -import kotlinx.serialization.Serializable - -@Serializable -data class TradesResponse( - val trades: List -) internal class TradesProcessorV2( private val tradeProcessor: TradeProcessorV2, private val limit: Int = TRADES_LIMIT, ) { fun processSubscribed( - payload: TradesResponse - ): List { - return payload.trades.mapNotNull { + existing: InternalMarketState, + payload: IndexerTradeResponse? + ): InternalMarketState { + existing.trades = payload?.trades?.mapNotNull { tradeProcessor.process(it) } + return existing } fun processChannelData( - existing: List?, - payload: TradesResponse, - ): List { - val new = payload.trades.mapNotNull { + existing: InternalMarketState, + payload: IndexerTradeResponse?, + ): InternalMarketState { + val new = payload?.trades?.mapNotNull { tradeProcessor.process(it) } - val merged = existing?.let { - mergeWithIds(new, existing) { trade -> trade.id } - } ?: new + val existingTrades = existing.trades + val merged = + if (new != null && existingTrades != null) { + mergeWithIds(new, existingTrades) { trade -> trade.id } + } else { + new ?: existingTrades ?: emptyList() + } - return if (merged.size > limit) { + existing.trades = if (merged.size > limit) { merged.subList(0, limit) } else { merged } + + return existing } } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Candles.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Candles.kt index 4c0b393dc..b15fc5f77 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Candles.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Candles.kt @@ -88,7 +88,6 @@ internal fun TradingStateMachine.receivedCandles( ): StateChanges { if (staticTyping) { val candlesPayload = parser.asTypedObject(payload) - print(candlesPayload) marketsProcessor.processCandles( existing = internalState.marketsSummary, marketId = marketId, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Trades.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Trades.kt index 42e416aac..4e4dab3b0 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Trades.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Trades.kt @@ -1,11 +1,10 @@ package exchange.dydx.abacus.state.model -import exchange.dydx.abacus.processor.markets.TradesResponse import exchange.dydx.abacus.protocols.asTypedObject import exchange.dydx.abacus.state.changes.Changes import exchange.dydx.abacus.state.changes.StateChanges -import exchange.dydx.abacus.state.internalstate.InternalMarketState import exchange.dydx.abacus.utils.toJson +import indexer.codegen.IndexerTradeResponse import kollections.iListOf internal fun TradingStateMachine.receivedTrades( @@ -13,17 +12,15 @@ internal fun TradingStateMachine.receivedTrades( payload: Map ): StateChanges? { return if (market != null) { - this.marketsSummary = marketsProcessor.receivedTradesDeprecated(marketsSummary, market, payload) if (staticTyping) { - val marketState = internalState.marketsSummary.markets[market] - val response = parser.asTypedObject(payload.toJson()) - val trades = response?.let { tradesProcessorV2.processSubscribed(it) } ?: emptyList() - - if (marketState != null) { - marketState.trades = trades - } else { - internalState.marketsSummary.markets[market] = InternalMarketState(trades) - } + val response = parser.asTypedObject(payload.toJson()) + marketsProcessor.processTradesSubscribed( + existing = internalState.marketsSummary, + marketId = market, + content = response, + ) + } else { + this.marketsSummary = marketsProcessor.receivedTradesDeprecated(marketsSummary, market, payload) } StateChanges(iListOf(Changes.trades), iListOf(market)) } else { @@ -36,9 +33,15 @@ internal fun TradingStateMachine.receivedTradesChanges( payload: Map ): StateChanges? { return if (market != null) { - this.marketsSummary = marketsProcessor.receivedTradesChangesDeprecated(marketsSummary, market, payload) if (staticTyping) { - processTradeUpdates(market, payload) + val response = parser.asTypedObject(payload.toJson()) + marketsProcessor.processTradesUpdates( + existing = internalState.marketsSummary, + marketId = market, + content = response, + ) + } else { + this.marketsSummary = marketsProcessor.receivedTradesChangesDeprecated(marketsSummary, market, payload) } StateChanges(iListOf(Changes.trades), iListOf(market)) } else { @@ -51,31 +54,20 @@ internal fun TradingStateMachine.receivedBatchedTradesChanges( payload: List ): StateChanges? { return if (market != null) { - this.marketsSummary = marketsProcessor.receivedBatchedTradesChanges(marketsSummary, market, payload) if (staticTyping) { payload.mapNotNull { parser.asNativeMap(it) }.forEach { tradeUpdatePayload -> - processTradeUpdates(market, tradeUpdatePayload) + val response = parser.asTypedObject(tradeUpdatePayload.toJson()) + marketsProcessor.processTradesUpdates( + existing = internalState.marketsSummary, + marketId = market, + content = response, + ) } + } else { + this.marketsSummary = marketsProcessor.receivedBatchedTradesChanges(marketsSummary, market, payload) } StateChanges(iListOf(Changes.trades), iListOf(market)) } else { null } } - -private fun TradingStateMachine.processTradeUpdates( - market: String, - payload: Map, -) { - val marketState = internalState.marketsSummary.markets[market] - val response = parser.asTypedObject(payload.toJson()) - val trades = response?.let { - tradesProcessorV2.processChannelData(marketState?.trades, it) - } ?: emptyList() - - if (marketState != null) { - marketState.trades = trades - } else { - internalState.marketsSummary.markets[market] = InternalMarketState(trades) - } -} diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt index 1b2fa40e1..76d3cc1cb 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt @@ -36,8 +36,6 @@ import exchange.dydx.abacus.processor.configs.ConfigsProcessor import exchange.dydx.abacus.processor.configs.RewardsParamsProcessor import exchange.dydx.abacus.processor.launchIncentive.LaunchIncentiveProcessor import exchange.dydx.abacus.processor.markets.MarketsSummaryProcessor -import exchange.dydx.abacus.processor.markets.TradeProcessorV2 -import exchange.dydx.abacus.processor.markets.TradesProcessorV2 import exchange.dydx.abacus.processor.router.IRouterProcessor import exchange.dydx.abacus.processor.router.skip.SkipProcessor import exchange.dydx.abacus.processor.router.squid.SquidProcessor @@ -102,9 +100,9 @@ open class TradingStateMachine( internal val parser: ParserProtocol = Parser() internal val marketsProcessor = MarketsSummaryProcessor( parser = parser, + localizer = localizer, staticTyping = staticTyping, ) - internal val tradesProcessorV2 = TradesProcessorV2(TradeProcessorV2(parser, localizer)) internal val assetsProcessor = run { val processor = AssetsProcessor( parser = parser, @@ -1142,15 +1140,20 @@ open class TradingStateMachine( if (markets != null) { val modified = trades?.toIMutableMap() ?: mutableMapOf() for (marketId in markets) { - val data = parser.asList( - parser.value( - data, - "markets.markets.$marketId.trades", - ), - ) as? IList> - val existing = trades?.get(marketId) - val trades = MarketTrade.create(existing, parser, data, localizer) - modified.typedSafeSet(marketId, trades) + if (staticTyping) { + val trades = internalState.marketsSummary.markets[marketId]?.trades + modified.typedSafeSet(marketId, trades?.toIList()) + } else { + val data = parser.asList( + parser.value( + data, + "markets.markets.$marketId.trades", + ), + ) as? IList> + val existing = trades?.get(marketId) + val trades = MarketTrade.create(existing, parser, data, localizer) + modified.typedSafeSet(marketId, trades) + } } trades = modified } else { diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/payload/BaseTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/payload/BaseTests.kt index 4fab80d0d..8ce4ebcb2 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/payload/BaseTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/payload/BaseTests.kt @@ -290,11 +290,17 @@ open class BaseTests( state?.historicalFundings, "historicalFundings", ) - verifyMarketsTradesState( - parser.asNativeMap(perp.marketsSummary?.get("markets")), - state?.trades, - "trades", - ) + if (staticTyping) { + for ((key, value) in perp.internalState.marketsSummary.markets) { + assertEquals(value.trades, state?.trades?.get(key)) + } + } else { + verifyMarketsTradesState( + parser.asNativeMap(perp.marketsSummary?.get("markets")), + state?.trades, + "trades", + ) + } verifyMarketsCandlesState( parser.asNativeMap(perp.marketsSummary?.get("markets")), state?.candles, diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v3/V3MarketsDelayedTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v3/V3MarketsDelayedTests.kt index 191ff1755..918ecac2f 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v3/V3MarketsDelayedTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v3/V3MarketsDelayedTests.kt @@ -4,6 +4,7 @@ import exchange.dydx.abacus.tests.extensions.loadTrades import kotlin.test.Test import kotlin.test.assertNotNull import kotlin.test.assertNull +import kotlin.test.assertTrue class V3MarketsDelayedTests : V3BaseTests() { @Test @@ -28,11 +29,16 @@ class V3MarketsDelayedTests : V3BaseTests() { assertNull(perp.state?.marketsSummary) }, ) - test( - { - perp.loadTrades(mock) - }, - """ + + if (perp.staticTyping) { + perp.loadTrades(mock) + assertNull(perp.state?.marketsSummary) + } else { + test( + { + perp.loadTrades(mock) + }, + """ { "markets": { "markets": { @@ -45,11 +51,13 @@ class V3MarketsDelayedTests : V3BaseTests() { } } } - """.trimIndent(), - { - assertNull(perp.state?.marketsSummary) - }, - ) + """.trimIndent(), + { + assertNull(perp.state?.marketsSummary) + }, + ) + } + test( { loadMarkets() @@ -74,11 +82,17 @@ class V3MarketsDelayedTests : V3BaseTests() { @Test fun testTradesFirst() { - test( - { - perp.loadTrades(mock) - }, - """ + if (perp.staticTyping) { + perp.loadTrades(mock) + val market = perp.internalState.marketsSummary.markets.get("ETH-USD") + assertTrue { market?.trades?.isNotEmpty() == true } + assertNull(perp.state?.marketsSummary) + } else { + test( + { + perp.loadTrades(mock) + }, + """ { "markets": { "markets": { @@ -89,16 +103,24 @@ class V3MarketsDelayedTests : V3BaseTests() { } } } - """.trimIndent(), - { - assertNull(perp.state?.marketsSummary) - }, - ) - test( - { - loadOrderbook() - }, - """ + """.trimIndent(), + { + assertNull(perp.state?.marketsSummary) + }, + ) + } + + if (perp.staticTyping) { + loadOrderbook() + val market = perp.internalState.marketsSummary.markets.get("ETH-USD") + assertTrue { market?.trades?.isNotEmpty() == true } + assertNull(perp.state?.marketsSummary) + } else { + test( + { + loadOrderbook() + }, + """ { "markets": { "markets": { @@ -111,16 +133,24 @@ class V3MarketsDelayedTests : V3BaseTests() { } } } - """.trimIndent(), - { - assertNull(perp.state?.marketsSummary) - }, - ) - test( - { - loadMarkets() - }, - """ + """.trimIndent(), + { + assertNull(perp.state?.marketsSummary) + }, + ) + } + + if (perp.staticTyping) { + loadMarkets() + val market = perp.internalState.marketsSummary.markets.get("ETH-USD") + assertTrue { market?.trades?.isNotEmpty() == true } + assertNotNull(perp.state?.marketsSummary) + } else { + test( + { + loadMarkets() + }, + """ { "markets": { "markets": { @@ -131,10 +161,11 @@ class V3MarketsDelayedTests : V3BaseTests() { } } } - """.trimIndent(), - { - assertNotNull(perp.state?.marketsSummary) - }, - ) + """.trimIndent(), + { + assertNotNull(perp.state?.marketsSummary) + }, + ) + } } } diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4DuplicateWebsocketMessageTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4DuplicateWebsocketMessageTests.kt index fdc563038..5768bc84a 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4DuplicateWebsocketMessageTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4DuplicateWebsocketMessageTests.kt @@ -2,6 +2,7 @@ package exchange.dydx.abacus.payload.v4 import exchange.dydx.abacus.tests.extensions.loadv4TradesChanged import kotlin.test.Test +import kotlin.test.assertEquals class V4DuplicateWebsocketMessageTests : V4BaseTests() { @@ -80,11 +81,20 @@ class V4DuplicateWebsocketMessageTests : V4BaseTests() { setup() repeat(2) { - test( - { - perp.loadv4TradesChanged(mock, testWsUrl) - }, - """ + if (perp.staticTyping) { + perp.loadv4TradesChanged(mock, testWsUrl) + val market = perp.internalState.marketsSummary.markets.get("ETH-USD") + assertEquals(1, market?.trades?.size) + val firstItem = market?.trades?.get(0) + assertEquals("8ee6d90d-272d-5edd-bf0f-2e4d6ae3d3b7", firstItem?.id) + assertEquals("BUY", firstItem?.side?.rawValue) + assertEquals(1.593707, firstItem?.size) + } else { + test( + { + perp.loadv4TradesChanged(mock, testWsUrl) + }, + """ { "markets":{ "markets":{ @@ -104,8 +114,9 @@ class V4DuplicateWebsocketMessageTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } } } diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4MarketsTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4MarketsTests.kt index 389430f0f..b9e51d072 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4MarketsTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4MarketsTests.kt @@ -455,11 +455,20 @@ class V4MarketsTests : V4BaseTests() { } private fun testTradesSubscribed() { - test( - { - perp.loadv4TradesSubscribed(mock, testWsUrl) - }, - """ + if (perp.staticTyping) { + perp.loadv4TradesSubscribed(mock, testWsUrl) + val trades = perp.state?.trades?.get("ETH-USD") + assertEquals(trades?.size, 100) + val firstItem = trades?.firstOrNull() + assertEquals(firstItem?.side?.rawValue, "SELL") + assertEquals(firstItem?.size, 9.5E-4) + assertEquals(firstItem?.price, 1255.98) + } else { + test( + { + perp.loadv4TradesSubscribed(mock, testWsUrl) + }, + """ { "markets":{ "markets":{ @@ -479,24 +488,34 @@ class V4MarketsTests : V4BaseTests() { } } } - """.trimIndent(), - { - val trades = - parser.asList(parser.value(perp.data, "markets.markets.ETH-USD.trades")) - assertEquals( - 100, - trades?.size, - ) - }, - ) + """.trimIndent(), + { + val trades = + parser.asList(parser.value(perp.data, "markets.markets.ETH-USD.trades")) + assertEquals( + 100, + trades?.size, + ) + }, + ) + } } private fun testTradesChanged() { - test( - { - perp.loadv4TradesChanged(mock, testWsUrl) - }, - """ + if (perp.staticTyping) { + perp.loadv4TradesChanged(mock, testWsUrl) + val trades = perp.state?.trades?.get("ETH-USD") + assertEquals(trades?.size, 101) + val firstItem = trades?.firstOrNull() + assertEquals(firstItem?.side?.rawValue, "BUY") + assertEquals(firstItem?.size, 1.593707) + assertEquals(firstItem?.price, 1255.949) + } else { + test( + { + perp.loadv4TradesChanged(mock, testWsUrl) + }, + """ { "markets":{ "markets":{ @@ -515,24 +534,34 @@ class V4MarketsTests : V4BaseTests() { } } } - """.trimIndent(), - { - val trades = - parser.asList(parser.value(perp.data, "markets.markets.ETH-USD.trades")) - assertEquals( - 101, - trades?.size, - ) - }, - ) + """.trimIndent(), + { + val trades = + parser.asList(parser.value(perp.data, "markets.markets.ETH-USD.trades")) + assertEquals( + 101, + trades?.size, + ) + }, + ) + } } private fun testTradesBatchChanged() { - test( - { - perp.loadv4TradesBatchChanged(mock, testWsUrl) - }, - """ + if (perp.staticTyping) { + perp.loadv4TradesBatchChanged(mock, testWsUrl) + val trades = perp.state?.trades?.get("ETH-USD") + assertEquals(trades?.size, 240) + val firstItem = trades?.firstOrNull() + assertEquals(firstItem?.side?.rawValue, "SELL") + assertEquals(firstItem?.size, 1.02E-4) + assertEquals(firstItem?.price, 1291.255) + } else { + test( + { + perp.loadv4TradesBatchChanged(mock, testWsUrl) + }, + """ { "markets":{ "markets":{ @@ -551,24 +580,30 @@ class V4MarketsTests : V4BaseTests() { } } } - """.trimIndent(), - { - val trades = - parser.asList(parser.value(perp.data, "markets.markets.ETH-USD.trades")) - assertEquals( - 240, - trades?.size, - ) - }, - ) + """.trimIndent(), + { + val trades = + parser.asList(parser.value(perp.data, "markets.markets.ETH-USD.trades")) + assertEquals( + 240, + trades?.size, + ) + }, + ) + } } private fun testMarketNotOnline() { - test( - { - perp.socket(testWsUrl, mock.marketsChannel.v4_oracle_price_not_in_list, 0, null) - }, - """ + if (perp.staticTyping) { + perp.loadv4MarketsSubscribed(mock, testWsUrl) + val trades = perp.state?.trades?.get("ETH-USD") + assertEquals(trades?.size, 240) + } else { + test( + { + perp.socket(testWsUrl, mock.marketsChannel.v4_oracle_price_not_in_list, 0, null) + }, + """ { "markets":{ "markets":{ @@ -578,16 +613,17 @@ class V4MarketsTests : V4BaseTests() { } } } - """.trimIndent(), - { - val trades = - parser.asList(parser.value(perp.data, "markets.markets.ETH-USD.trades")) - assertEquals( - 240, - trades?.size, - ) - }, - ) + """.trimIndent(), + { + val trades = + parser.asList(parser.value(perp.data, "markets.markets.ETH-USD.trades")) + assertEquals( + 240, + trades?.size, + ) + }, + ) + } } @Test diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/processor/markets/TradesProcessorV2Tests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/processor/markets/TradesProcessorV2Tests.kt index 8095a0f63..f1eb20aa2 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/processor/markets/TradesProcessorV2Tests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/processor/markets/TradesProcessorV2Tests.kt @@ -3,9 +3,11 @@ package exchange.dydx.abacus.processor.markets import exchange.dydx.abacus.output.MarketTrade import exchange.dydx.abacus.output.MarketTradeResources import exchange.dydx.abacus.output.input.OrderSide +import exchange.dydx.abacus.state.internalstate.InternalMarketState import exchange.dydx.abacus.utils.DummyLocalizer import exchange.dydx.abacus.utils.Parser import indexer.codegen.IndexerOrderSide +import indexer.codegen.IndexerTradeResponse import indexer.codegen.IndexerTradeResponseObject import kotlin.test.Test import kotlin.test.assertContentEquals @@ -18,15 +20,16 @@ class TradesProcessorV2Tests { ) @Test fun testSubscribed_happyPath() { - val payload = TradesResponse( - listOf( + val payload = IndexerTradeResponse( + trades = arrayOf( indexerTrade("1"), indexerTrade("2"), indexerTrade("3"), ), ) - val trades = processor.processSubscribed(payload) + val state = InternalMarketState() + processor.processSubscribed(state, payload) assertContentEquals( listOf( @@ -34,13 +37,13 @@ class TradesProcessorV2Tests { expectedMarketTrade("2"), expectedMarketTrade("3"), ), - trades, + state.trades, ) } @Test fun testSubscribed_invalidTrades() { - val payload = TradesResponse( - listOf( + val payload = IndexerTradeResponse( + trades = arrayOf( indexerTrade("1"), IndexerTradeResponseObject( id = "2", @@ -54,32 +57,34 @@ class TradesProcessorV2Tests { ), ) - val trades = processor.processSubscribed(payload) + val state = InternalMarketState() + processor.processSubscribed(state, payload) assertContentEquals( listOf( expectedMarketTrade("1"), ), - trades, + state.trades, ) } @Test fun testChannelData_happyPath() { - val subscribedPayload = TradesResponse( - listOf( + val subscribedPayload = IndexerTradeResponse( + trades = arrayOf( indexerTrade("1"), indexerTrade("2"), ), ) - val initialTrades = processor.processSubscribed(subscribedPayload) - val channelPayload = TradesResponse( - listOf( + val state = InternalMarketState() + processor.processSubscribed(state, subscribedPayload) + val channelPayload = IndexerTradeResponse( + trades = arrayOf( indexerTrade("3"), indexerTrade("4"), ), ) - val finalTrades = processor.processChannelData(initialTrades, channelPayload) + processor.processChannelData(state, channelPayload) assertContentEquals( listOf( @@ -88,20 +93,21 @@ class TradesProcessorV2Tests { expectedMarketTrade("1"), expectedMarketTrade("2"), ), - finalTrades, + state.trades, ) } @Test fun testChannelData_overTradesLimit() { - val subscribedPayload = TradesResponse( - listOf( + val subscribedPayload = IndexerTradeResponse( + trades = arrayOf( indexerTrade("1"), indexerTrade("2"), ), ) - val initialTrades = processor.processSubscribed(subscribedPayload) - val channelPayload = TradesResponse( - listOf( + val state = InternalMarketState() + processor.processSubscribed(state, subscribedPayload) + val channelPayload = IndexerTradeResponse( + trades = arrayOf( indexerTrade("3"), indexerTrade("4"), indexerTrade("5"), @@ -109,7 +115,7 @@ class TradesProcessorV2Tests { ), ) - val finalTrades = processor.processChannelData(initialTrades, channelPayload) + processor.processChannelData(state, channelPayload) assertContentEquals( listOf( @@ -119,26 +125,29 @@ class TradesProcessorV2Tests { expectedMarketTrade("6"), expectedMarketTrade("1"), ), - finalTrades, + state.trades, ) } @Test fun testChannelData_duplicateIds() { - val subscribedPayload = TradesResponse( - listOf( + val subscribedPayload = IndexerTradeResponse( + trades = arrayOf( indexerTrade("1"), indexerTrade("2"), ), ) - val initialTrades = processor.processSubscribed(subscribedPayload) - val channelPayload = TradesResponse( - listOf( + val state = InternalMarketState() + processor.processSubscribed(state, subscribedPayload) + val initialTrades = state.trades + val channelPayload = IndexerTradeResponse( + trades = arrayOf( indexerTrade("1").copy(size = "500.0"), indexerTrade("2").copy(size = "500.0"), ), ) - val finalTrades = processor.processChannelData(initialTrades, channelPayload) + processor.processChannelData(state, channelPayload) + val finalTrades = state.trades assertContentEquals( listOf( From 5ce9fdb4d3e2813e587b21aa88cbb10df3153a51 Mon Sep 17 00:00:00 2001 From: Rui Date: Tue, 6 Aug 2024 18:36:35 -0700 Subject: [PATCH 13/63] Revert "Merge branch 'feature/markets_4' of github.com:dydxprotocol/v4-abacus into feature/markets_4" This reverts commit 60d8eb6db254c414a3b995ee358abdb2419aa14b, reversing changes made to d52adce850f2beb325d7b11c436f90ff63cfaf9a. --- build.gradle.kts | 2 +- v4_abacus.podspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index a030a5024..71ee6457e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -52,7 +52,7 @@ allprojects { } group = "exchange.dydx.abacus" -version = "1.8.78" +version = "1.8.77" repositories { google() diff --git a/v4_abacus.podspec b/v4_abacus.podspec index 44c131e45..d61c9d1fd 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.78' + spec.version = '1.8.77' spec.homepage = 'https://github.com/dydxprotocol/v4-abacus' spec.source = { :http=> ''} spec.authors = '' From c4c373062766c1b32b0a4c9f0c1ff53f968cbb13 Mon Sep 17 00:00:00 2001 From: Rui Date: Tue, 6 Aug 2024 18:36:54 -0700 Subject: [PATCH 14/63] Revert "Add market trades to the output state." This reverts commit d52adce850f2beb325d7b11c436f90ff63cfaf9a. --- .../markets/MarketsSummaryProcessor.kt | 30 ---- ...adesProcessorV2.kt => TradeProcessorV2.kt} | 38 ++--- .../model/TradingStateMachine+Candles.kt | 1 + .../state/model/TradingStateMachine+Trades.kt | 58 ++++--- .../state/model/TradingStateMachine.kt | 27 ++- .../exchange.dydx.abacus/payload/BaseTests.kt | 16 +- .../payload/v3/V3MarketsDelayedTests.kt | 111 +++++-------- .../v4/V4DuplicateWebsocketMessageTests.kt | 25 +-- .../payload/v4/V4MarketsTests.kt | 156 +++++++----------- .../markets/TradesProcessorV2Tests.kt | 65 ++++---- 10 files changed, 203 insertions(+), 324 deletions(-) rename src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/{TradesProcessorV2.kt => TradeProcessorV2.kt} (74%) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsSummaryProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsSummaryProcessor.kt index ebf000acf..3ea147606 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsSummaryProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsSummaryProcessor.kt @@ -1,7 +1,6 @@ package exchange.dydx.abacus.processor.markets import exchange.dydx.abacus.processor.base.BaseProcessor -import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.internalstate.InternalMarketState import exchange.dydx.abacus.state.internalstate.InternalMarketSummaryState @@ -10,21 +9,18 @@ import exchange.dydx.abacus.utils.safeSet import indexer.codegen.IndexerCandleResponse import indexer.codegen.IndexerCandleResponseObject import indexer.codegen.IndexerOrderbookResponseObject -import indexer.codegen.IndexerTradeResponse import indexer.models.IndexerCompositeMarketObject import indexer.models.IndexerWsMarketUpdateResponse import indexer.models.IndexerWsOrderbookUpdateResponse internal class MarketsSummaryProcessor( parser: ParserProtocol, - localizer: LocalizerProtocol?, calculateSparklines: Boolean = false, private val staticTyping: Boolean, ) : BaseProcessor(parser) { private val marketsProcessor = MarketsProcessor(parser, calculateSparklines) private val orderbookProcessor = OrderbookProcessor(parser) private val candlesProcessor = CandlesProcessor(parser) - private val tradesProcessor = TradesProcessorV2(TradeProcessorV2(parser, localizer)) internal var groupingMultiplier: Int get() = if (staticTyping) orderbookProcessor.groupingMultiplier else marketsProcessor.groupingMultiplier @@ -155,32 +151,6 @@ internal class MarketsSummaryProcessor( return existing } - fun processTradesSubscribed( - existing: InternalMarketSummaryState, - marketId: String, - content: IndexerTradeResponse? - ): InternalMarketSummaryState { - val marketState = existing.markets[marketId] ?: InternalMarketState() - existing.markets[marketId] = tradesProcessor.processSubscribed( - existing = marketState, - payload = content, - ) - return existing - } - - fun processTradesUpdates( - existing: InternalMarketSummaryState, - marketId: String, - content: IndexerTradeResponse? - ): InternalMarketSummaryState { - val marketState = existing.markets[marketId] ?: InternalMarketState() - existing.markets[marketId] = tradesProcessor.processChannelData( - existing = marketState, - payload = content, - ) - return existing - } - internal fun subscribedDeprecated( existing: Map?, content: Map diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/TradesProcessorV2.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/TradeProcessorV2.kt similarity index 74% rename from src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/TradesProcessorV2.kt rename to src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/TradeProcessorV2.kt index 6dd42cd54..338bd0de4 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/TradesProcessorV2.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/TradeProcessorV2.kt @@ -7,49 +7,45 @@ import exchange.dydx.abacus.output.input.OrderType import exchange.dydx.abacus.processor.base.mergeWithIds import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol -import exchange.dydx.abacus.state.internalstate.InternalMarketState import exchange.dydx.abacus.utils.IndexerResponseParsingException import exchange.dydx.abacus.utils.Logger import exchange.dydx.abacus.utils.parseException -import indexer.codegen.IndexerTradeResponse import indexer.codegen.IndexerTradeResponseObject +import kotlinx.serialization.Serializable + +@Serializable +data class TradesResponse( + val trades: List +) internal class TradesProcessorV2( private val tradeProcessor: TradeProcessorV2, private val limit: Int = TRADES_LIMIT, ) { fun processSubscribed( - existing: InternalMarketState, - payload: IndexerTradeResponse? - ): InternalMarketState { - existing.trades = payload?.trades?.mapNotNull { + payload: TradesResponse + ): List { + return payload.trades.mapNotNull { tradeProcessor.process(it) } - return existing } fun processChannelData( - existing: InternalMarketState, - payload: IndexerTradeResponse?, - ): InternalMarketState { - val new = payload?.trades?.mapNotNull { + existing: List?, + payload: TradesResponse, + ): List { + val new = payload.trades.mapNotNull { tradeProcessor.process(it) } - val existingTrades = existing.trades - val merged = - if (new != null && existingTrades != null) { - mergeWithIds(new, existingTrades) { trade -> trade.id } - } else { - new ?: existingTrades ?: emptyList() - } + val merged = existing?.let { + mergeWithIds(new, existing) { trade -> trade.id } + } ?: new - existing.trades = if (merged.size > limit) { + return if (merged.size > limit) { merged.subList(0, limit) } else { merged } - - return existing } } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Candles.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Candles.kt index b15fc5f77..4c0b393dc 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Candles.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Candles.kt @@ -88,6 +88,7 @@ internal fun TradingStateMachine.receivedCandles( ): StateChanges { if (staticTyping) { val candlesPayload = parser.asTypedObject(payload) + print(candlesPayload) marketsProcessor.processCandles( existing = internalState.marketsSummary, marketId = marketId, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Trades.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Trades.kt index 4e4dab3b0..42e416aac 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Trades.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Trades.kt @@ -1,10 +1,11 @@ package exchange.dydx.abacus.state.model +import exchange.dydx.abacus.processor.markets.TradesResponse import exchange.dydx.abacus.protocols.asTypedObject import exchange.dydx.abacus.state.changes.Changes import exchange.dydx.abacus.state.changes.StateChanges +import exchange.dydx.abacus.state.internalstate.InternalMarketState import exchange.dydx.abacus.utils.toJson -import indexer.codegen.IndexerTradeResponse import kollections.iListOf internal fun TradingStateMachine.receivedTrades( @@ -12,15 +13,17 @@ internal fun TradingStateMachine.receivedTrades( payload: Map ): StateChanges? { return if (market != null) { + this.marketsSummary = marketsProcessor.receivedTradesDeprecated(marketsSummary, market, payload) if (staticTyping) { - val response = parser.asTypedObject(payload.toJson()) - marketsProcessor.processTradesSubscribed( - existing = internalState.marketsSummary, - marketId = market, - content = response, - ) - } else { - this.marketsSummary = marketsProcessor.receivedTradesDeprecated(marketsSummary, market, payload) + val marketState = internalState.marketsSummary.markets[market] + val response = parser.asTypedObject(payload.toJson()) + val trades = response?.let { tradesProcessorV2.processSubscribed(it) } ?: emptyList() + + if (marketState != null) { + marketState.trades = trades + } else { + internalState.marketsSummary.markets[market] = InternalMarketState(trades) + } } StateChanges(iListOf(Changes.trades), iListOf(market)) } else { @@ -33,15 +36,9 @@ internal fun TradingStateMachine.receivedTradesChanges( payload: Map ): StateChanges? { return if (market != null) { + this.marketsSummary = marketsProcessor.receivedTradesChangesDeprecated(marketsSummary, market, payload) if (staticTyping) { - val response = parser.asTypedObject(payload.toJson()) - marketsProcessor.processTradesUpdates( - existing = internalState.marketsSummary, - marketId = market, - content = response, - ) - } else { - this.marketsSummary = marketsProcessor.receivedTradesChangesDeprecated(marketsSummary, market, payload) + processTradeUpdates(market, payload) } StateChanges(iListOf(Changes.trades), iListOf(market)) } else { @@ -54,20 +51,31 @@ internal fun TradingStateMachine.receivedBatchedTradesChanges( payload: List ): StateChanges? { return if (market != null) { + this.marketsSummary = marketsProcessor.receivedBatchedTradesChanges(marketsSummary, market, payload) if (staticTyping) { payload.mapNotNull { parser.asNativeMap(it) }.forEach { tradeUpdatePayload -> - val response = parser.asTypedObject(tradeUpdatePayload.toJson()) - marketsProcessor.processTradesUpdates( - existing = internalState.marketsSummary, - marketId = market, - content = response, - ) + processTradeUpdates(market, tradeUpdatePayload) } - } else { - this.marketsSummary = marketsProcessor.receivedBatchedTradesChanges(marketsSummary, market, payload) } StateChanges(iListOf(Changes.trades), iListOf(market)) } else { null } } + +private fun TradingStateMachine.processTradeUpdates( + market: String, + payload: Map, +) { + val marketState = internalState.marketsSummary.markets[market] + val response = parser.asTypedObject(payload.toJson()) + val trades = response?.let { + tradesProcessorV2.processChannelData(marketState?.trades, it) + } ?: emptyList() + + if (marketState != null) { + marketState.trades = trades + } else { + internalState.marketsSummary.markets[market] = InternalMarketState(trades) + } +} diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt index 76d3cc1cb..1b2fa40e1 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt @@ -36,6 +36,8 @@ import exchange.dydx.abacus.processor.configs.ConfigsProcessor import exchange.dydx.abacus.processor.configs.RewardsParamsProcessor import exchange.dydx.abacus.processor.launchIncentive.LaunchIncentiveProcessor import exchange.dydx.abacus.processor.markets.MarketsSummaryProcessor +import exchange.dydx.abacus.processor.markets.TradeProcessorV2 +import exchange.dydx.abacus.processor.markets.TradesProcessorV2 import exchange.dydx.abacus.processor.router.IRouterProcessor import exchange.dydx.abacus.processor.router.skip.SkipProcessor import exchange.dydx.abacus.processor.router.squid.SquidProcessor @@ -100,9 +102,9 @@ open class TradingStateMachine( internal val parser: ParserProtocol = Parser() internal val marketsProcessor = MarketsSummaryProcessor( parser = parser, - localizer = localizer, staticTyping = staticTyping, ) + internal val tradesProcessorV2 = TradesProcessorV2(TradeProcessorV2(parser, localizer)) internal val assetsProcessor = run { val processor = AssetsProcessor( parser = parser, @@ -1140,20 +1142,15 @@ open class TradingStateMachine( if (markets != null) { val modified = trades?.toIMutableMap() ?: mutableMapOf() for (marketId in markets) { - if (staticTyping) { - val trades = internalState.marketsSummary.markets[marketId]?.trades - modified.typedSafeSet(marketId, trades?.toIList()) - } else { - val data = parser.asList( - parser.value( - data, - "markets.markets.$marketId.trades", - ), - ) as? IList> - val existing = trades?.get(marketId) - val trades = MarketTrade.create(existing, parser, data, localizer) - modified.typedSafeSet(marketId, trades) - } + val data = parser.asList( + parser.value( + data, + "markets.markets.$marketId.trades", + ), + ) as? IList> + val existing = trades?.get(marketId) + val trades = MarketTrade.create(existing, parser, data, localizer) + modified.typedSafeSet(marketId, trades) } trades = modified } else { diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/payload/BaseTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/payload/BaseTests.kt index 8ce4ebcb2..4fab80d0d 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/payload/BaseTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/payload/BaseTests.kt @@ -290,17 +290,11 @@ open class BaseTests( state?.historicalFundings, "historicalFundings", ) - if (staticTyping) { - for ((key, value) in perp.internalState.marketsSummary.markets) { - assertEquals(value.trades, state?.trades?.get(key)) - } - } else { - verifyMarketsTradesState( - parser.asNativeMap(perp.marketsSummary?.get("markets")), - state?.trades, - "trades", - ) - } + verifyMarketsTradesState( + parser.asNativeMap(perp.marketsSummary?.get("markets")), + state?.trades, + "trades", + ) verifyMarketsCandlesState( parser.asNativeMap(perp.marketsSummary?.get("markets")), state?.candles, diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v3/V3MarketsDelayedTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v3/V3MarketsDelayedTests.kt index 918ecac2f..191ff1755 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v3/V3MarketsDelayedTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v3/V3MarketsDelayedTests.kt @@ -4,7 +4,6 @@ import exchange.dydx.abacus.tests.extensions.loadTrades import kotlin.test.Test import kotlin.test.assertNotNull import kotlin.test.assertNull -import kotlin.test.assertTrue class V3MarketsDelayedTests : V3BaseTests() { @Test @@ -29,16 +28,11 @@ class V3MarketsDelayedTests : V3BaseTests() { assertNull(perp.state?.marketsSummary) }, ) - - if (perp.staticTyping) { - perp.loadTrades(mock) - assertNull(perp.state?.marketsSummary) - } else { - test( - { - perp.loadTrades(mock) - }, - """ + test( + { + perp.loadTrades(mock) + }, + """ { "markets": { "markets": { @@ -51,13 +45,11 @@ class V3MarketsDelayedTests : V3BaseTests() { } } } - """.trimIndent(), - { - assertNull(perp.state?.marketsSummary) - }, - ) - } - + """.trimIndent(), + { + assertNull(perp.state?.marketsSummary) + }, + ) test( { loadMarkets() @@ -82,17 +74,11 @@ class V3MarketsDelayedTests : V3BaseTests() { @Test fun testTradesFirst() { - if (perp.staticTyping) { - perp.loadTrades(mock) - val market = perp.internalState.marketsSummary.markets.get("ETH-USD") - assertTrue { market?.trades?.isNotEmpty() == true } - assertNull(perp.state?.marketsSummary) - } else { - test( - { - perp.loadTrades(mock) - }, - """ + test( + { + perp.loadTrades(mock) + }, + """ { "markets": { "markets": { @@ -103,24 +89,16 @@ class V3MarketsDelayedTests : V3BaseTests() { } } } - """.trimIndent(), - { - assertNull(perp.state?.marketsSummary) - }, - ) - } - - if (perp.staticTyping) { - loadOrderbook() - val market = perp.internalState.marketsSummary.markets.get("ETH-USD") - assertTrue { market?.trades?.isNotEmpty() == true } - assertNull(perp.state?.marketsSummary) - } else { - test( - { - loadOrderbook() - }, - """ + """.trimIndent(), + { + assertNull(perp.state?.marketsSummary) + }, + ) + test( + { + loadOrderbook() + }, + """ { "markets": { "markets": { @@ -133,24 +111,16 @@ class V3MarketsDelayedTests : V3BaseTests() { } } } - """.trimIndent(), - { - assertNull(perp.state?.marketsSummary) - }, - ) - } - - if (perp.staticTyping) { - loadMarkets() - val market = perp.internalState.marketsSummary.markets.get("ETH-USD") - assertTrue { market?.trades?.isNotEmpty() == true } - assertNotNull(perp.state?.marketsSummary) - } else { - test( - { - loadMarkets() - }, - """ + """.trimIndent(), + { + assertNull(perp.state?.marketsSummary) + }, + ) + test( + { + loadMarkets() + }, + """ { "markets": { "markets": { @@ -161,11 +131,10 @@ class V3MarketsDelayedTests : V3BaseTests() { } } } - """.trimIndent(), - { - assertNotNull(perp.state?.marketsSummary) - }, - ) - } + """.trimIndent(), + { + assertNotNull(perp.state?.marketsSummary) + }, + ) } } diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4DuplicateWebsocketMessageTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4DuplicateWebsocketMessageTests.kt index 5768bc84a..fdc563038 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4DuplicateWebsocketMessageTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4DuplicateWebsocketMessageTests.kt @@ -2,7 +2,6 @@ package exchange.dydx.abacus.payload.v4 import exchange.dydx.abacus.tests.extensions.loadv4TradesChanged import kotlin.test.Test -import kotlin.test.assertEquals class V4DuplicateWebsocketMessageTests : V4BaseTests() { @@ -81,20 +80,11 @@ class V4DuplicateWebsocketMessageTests : V4BaseTests() { setup() repeat(2) { - if (perp.staticTyping) { - perp.loadv4TradesChanged(mock, testWsUrl) - val market = perp.internalState.marketsSummary.markets.get("ETH-USD") - assertEquals(1, market?.trades?.size) - val firstItem = market?.trades?.get(0) - assertEquals("8ee6d90d-272d-5edd-bf0f-2e4d6ae3d3b7", firstItem?.id) - assertEquals("BUY", firstItem?.side?.rawValue) - assertEquals(1.593707, firstItem?.size) - } else { - test( - { - perp.loadv4TradesChanged(mock, testWsUrl) - }, - """ + test( + { + perp.loadv4TradesChanged(mock, testWsUrl) + }, + """ { "markets":{ "markets":{ @@ -114,9 +104,8 @@ class V4DuplicateWebsocketMessageTests : V4BaseTests() { } } } - """.trimIndent(), - ) - } + """.trimIndent(), + ) } } } diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4MarketsTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4MarketsTests.kt index b9e51d072..389430f0f 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4MarketsTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4MarketsTests.kt @@ -455,20 +455,11 @@ class V4MarketsTests : V4BaseTests() { } private fun testTradesSubscribed() { - if (perp.staticTyping) { - perp.loadv4TradesSubscribed(mock, testWsUrl) - val trades = perp.state?.trades?.get("ETH-USD") - assertEquals(trades?.size, 100) - val firstItem = trades?.firstOrNull() - assertEquals(firstItem?.side?.rawValue, "SELL") - assertEquals(firstItem?.size, 9.5E-4) - assertEquals(firstItem?.price, 1255.98) - } else { - test( - { - perp.loadv4TradesSubscribed(mock, testWsUrl) - }, - """ + test( + { + perp.loadv4TradesSubscribed(mock, testWsUrl) + }, + """ { "markets":{ "markets":{ @@ -488,34 +479,24 @@ class V4MarketsTests : V4BaseTests() { } } } - """.trimIndent(), - { - val trades = - parser.asList(parser.value(perp.data, "markets.markets.ETH-USD.trades")) - assertEquals( - 100, - trades?.size, - ) - }, - ) - } + """.trimIndent(), + { + val trades = + parser.asList(parser.value(perp.data, "markets.markets.ETH-USD.trades")) + assertEquals( + 100, + trades?.size, + ) + }, + ) } private fun testTradesChanged() { - if (perp.staticTyping) { - perp.loadv4TradesChanged(mock, testWsUrl) - val trades = perp.state?.trades?.get("ETH-USD") - assertEquals(trades?.size, 101) - val firstItem = trades?.firstOrNull() - assertEquals(firstItem?.side?.rawValue, "BUY") - assertEquals(firstItem?.size, 1.593707) - assertEquals(firstItem?.price, 1255.949) - } else { - test( - { - perp.loadv4TradesChanged(mock, testWsUrl) - }, - """ + test( + { + perp.loadv4TradesChanged(mock, testWsUrl) + }, + """ { "markets":{ "markets":{ @@ -534,34 +515,24 @@ class V4MarketsTests : V4BaseTests() { } } } - """.trimIndent(), - { - val trades = - parser.asList(parser.value(perp.data, "markets.markets.ETH-USD.trades")) - assertEquals( - 101, - trades?.size, - ) - }, - ) - } + """.trimIndent(), + { + val trades = + parser.asList(parser.value(perp.data, "markets.markets.ETH-USD.trades")) + assertEquals( + 101, + trades?.size, + ) + }, + ) } private fun testTradesBatchChanged() { - if (perp.staticTyping) { - perp.loadv4TradesBatchChanged(mock, testWsUrl) - val trades = perp.state?.trades?.get("ETH-USD") - assertEquals(trades?.size, 240) - val firstItem = trades?.firstOrNull() - assertEquals(firstItem?.side?.rawValue, "SELL") - assertEquals(firstItem?.size, 1.02E-4) - assertEquals(firstItem?.price, 1291.255) - } else { - test( - { - perp.loadv4TradesBatchChanged(mock, testWsUrl) - }, - """ + test( + { + perp.loadv4TradesBatchChanged(mock, testWsUrl) + }, + """ { "markets":{ "markets":{ @@ -580,30 +551,24 @@ class V4MarketsTests : V4BaseTests() { } } } - """.trimIndent(), - { - val trades = - parser.asList(parser.value(perp.data, "markets.markets.ETH-USD.trades")) - assertEquals( - 240, - trades?.size, - ) - }, - ) - } + """.trimIndent(), + { + val trades = + parser.asList(parser.value(perp.data, "markets.markets.ETH-USD.trades")) + assertEquals( + 240, + trades?.size, + ) + }, + ) } private fun testMarketNotOnline() { - if (perp.staticTyping) { - perp.loadv4MarketsSubscribed(mock, testWsUrl) - val trades = perp.state?.trades?.get("ETH-USD") - assertEquals(trades?.size, 240) - } else { - test( - { - perp.socket(testWsUrl, mock.marketsChannel.v4_oracle_price_not_in_list, 0, null) - }, - """ + test( + { + perp.socket(testWsUrl, mock.marketsChannel.v4_oracle_price_not_in_list, 0, null) + }, + """ { "markets":{ "markets":{ @@ -613,17 +578,16 @@ class V4MarketsTests : V4BaseTests() { } } } - """.trimIndent(), - { - val trades = - parser.asList(parser.value(perp.data, "markets.markets.ETH-USD.trades")) - assertEquals( - 240, - trades?.size, - ) - }, - ) - } + """.trimIndent(), + { + val trades = + parser.asList(parser.value(perp.data, "markets.markets.ETH-USD.trades")) + assertEquals( + 240, + trades?.size, + ) + }, + ) } @Test diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/processor/markets/TradesProcessorV2Tests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/processor/markets/TradesProcessorV2Tests.kt index f1eb20aa2..8095a0f63 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/processor/markets/TradesProcessorV2Tests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/processor/markets/TradesProcessorV2Tests.kt @@ -3,11 +3,9 @@ package exchange.dydx.abacus.processor.markets import exchange.dydx.abacus.output.MarketTrade import exchange.dydx.abacus.output.MarketTradeResources import exchange.dydx.abacus.output.input.OrderSide -import exchange.dydx.abacus.state.internalstate.InternalMarketState import exchange.dydx.abacus.utils.DummyLocalizer import exchange.dydx.abacus.utils.Parser import indexer.codegen.IndexerOrderSide -import indexer.codegen.IndexerTradeResponse import indexer.codegen.IndexerTradeResponseObject import kotlin.test.Test import kotlin.test.assertContentEquals @@ -20,16 +18,15 @@ class TradesProcessorV2Tests { ) @Test fun testSubscribed_happyPath() { - val payload = IndexerTradeResponse( - trades = arrayOf( + val payload = TradesResponse( + listOf( indexerTrade("1"), indexerTrade("2"), indexerTrade("3"), ), ) - val state = InternalMarketState() - processor.processSubscribed(state, payload) + val trades = processor.processSubscribed(payload) assertContentEquals( listOf( @@ -37,13 +34,13 @@ class TradesProcessorV2Tests { expectedMarketTrade("2"), expectedMarketTrade("3"), ), - state.trades, + trades, ) } @Test fun testSubscribed_invalidTrades() { - val payload = IndexerTradeResponse( - trades = arrayOf( + val payload = TradesResponse( + listOf( indexerTrade("1"), IndexerTradeResponseObject( id = "2", @@ -57,34 +54,32 @@ class TradesProcessorV2Tests { ), ) - val state = InternalMarketState() - processor.processSubscribed(state, payload) + val trades = processor.processSubscribed(payload) assertContentEquals( listOf( expectedMarketTrade("1"), ), - state.trades, + trades, ) } @Test fun testChannelData_happyPath() { - val subscribedPayload = IndexerTradeResponse( - trades = arrayOf( + val subscribedPayload = TradesResponse( + listOf( indexerTrade("1"), indexerTrade("2"), ), ) - val state = InternalMarketState() - processor.processSubscribed(state, subscribedPayload) - val channelPayload = IndexerTradeResponse( - trades = arrayOf( + val initialTrades = processor.processSubscribed(subscribedPayload) + val channelPayload = TradesResponse( + listOf( indexerTrade("3"), indexerTrade("4"), ), ) - processor.processChannelData(state, channelPayload) + val finalTrades = processor.processChannelData(initialTrades, channelPayload) assertContentEquals( listOf( @@ -93,21 +88,20 @@ class TradesProcessorV2Tests { expectedMarketTrade("1"), expectedMarketTrade("2"), ), - state.trades, + finalTrades, ) } @Test fun testChannelData_overTradesLimit() { - val subscribedPayload = IndexerTradeResponse( - trades = arrayOf( + val subscribedPayload = TradesResponse( + listOf( indexerTrade("1"), indexerTrade("2"), ), ) - val state = InternalMarketState() - processor.processSubscribed(state, subscribedPayload) - val channelPayload = IndexerTradeResponse( - trades = arrayOf( + val initialTrades = processor.processSubscribed(subscribedPayload) + val channelPayload = TradesResponse( + listOf( indexerTrade("3"), indexerTrade("4"), indexerTrade("5"), @@ -115,7 +109,7 @@ class TradesProcessorV2Tests { ), ) - processor.processChannelData(state, channelPayload) + val finalTrades = processor.processChannelData(initialTrades, channelPayload) assertContentEquals( listOf( @@ -125,29 +119,26 @@ class TradesProcessorV2Tests { expectedMarketTrade("6"), expectedMarketTrade("1"), ), - state.trades, + finalTrades, ) } @Test fun testChannelData_duplicateIds() { - val subscribedPayload = IndexerTradeResponse( - trades = arrayOf( + val subscribedPayload = TradesResponse( + listOf( indexerTrade("1"), indexerTrade("2"), ), ) - val state = InternalMarketState() - processor.processSubscribed(state, subscribedPayload) - val initialTrades = state.trades - val channelPayload = IndexerTradeResponse( - trades = arrayOf( + val initialTrades = processor.processSubscribed(subscribedPayload) + val channelPayload = TradesResponse( + listOf( indexerTrade("1").copy(size = "500.0"), indexerTrade("2").copy(size = "500.0"), ), ) - processor.processChannelData(state, channelPayload) - val finalTrades = state.trades + val finalTrades = processor.processChannelData(initialTrades, channelPayload) assertContentEquals( listOf( From 7a9ce0a894b9bc5e43a9a01cc102cca06ff59f86 Mon Sep 17 00:00:00 2001 From: mobile-build-bot-git Date: Wed, 7 Aug 2024 01:37:26 +0000 Subject: [PATCH 15/63] Bump version --- build.gradle.kts | 2 +- v4_abacus.podspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 71ee6457e..a030a5024 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -52,7 +52,7 @@ allprojects { } group = "exchange.dydx.abacus" -version = "1.8.77" +version = "1.8.78" repositories { google() diff --git a/v4_abacus.podspec b/v4_abacus.podspec index d61c9d1fd..44c131e45 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.77' + spec.version = '1.8.78' spec.homepage = 'https://github.com/dydxprotocol/v4-abacus' spec.source = { :http=> ''} spec.authors = '' From a688b82c1fad05cdd701bd7d5d5b9db0cc515c37 Mon Sep 17 00:00:00 2001 From: Rui Date: Tue, 6 Aug 2024 18:27:58 -0700 Subject: [PATCH 16/63] Add market trades to the output state. --- .../markets/MarketsSummaryProcessor.kt | 30 ++++ ...adeProcessorV2.kt => TradesProcessorV2.kt} | 38 +++-- .../model/TradingStateMachine+Candles.kt | 1 - .../state/model/TradingStateMachine+Trades.kt | 58 +++---- .../state/model/TradingStateMachine.kt | 27 +-- .../exchange.dydx.abacus/payload/BaseTests.kt | 16 +- .../payload/v3/V3MarketsDelayedTests.kt | 111 ++++++++----- .../v4/V4DuplicateWebsocketMessageTests.kt | 25 ++- .../payload/v4/V4MarketsTests.kt | 156 +++++++++++------- .../markets/TradesProcessorV2Tests.kt | 65 ++++---- 10 files changed, 324 insertions(+), 203 deletions(-) rename src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/{TradeProcessorV2.kt => TradesProcessorV2.kt} (74%) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsSummaryProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsSummaryProcessor.kt index 3ea147606..ebf000acf 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsSummaryProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketsSummaryProcessor.kt @@ -1,6 +1,7 @@ package exchange.dydx.abacus.processor.markets import exchange.dydx.abacus.processor.base.BaseProcessor +import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.internalstate.InternalMarketState import exchange.dydx.abacus.state.internalstate.InternalMarketSummaryState @@ -9,18 +10,21 @@ import exchange.dydx.abacus.utils.safeSet import indexer.codegen.IndexerCandleResponse import indexer.codegen.IndexerCandleResponseObject import indexer.codegen.IndexerOrderbookResponseObject +import indexer.codegen.IndexerTradeResponse import indexer.models.IndexerCompositeMarketObject import indexer.models.IndexerWsMarketUpdateResponse import indexer.models.IndexerWsOrderbookUpdateResponse internal class MarketsSummaryProcessor( parser: ParserProtocol, + localizer: LocalizerProtocol?, calculateSparklines: Boolean = false, private val staticTyping: Boolean, ) : BaseProcessor(parser) { private val marketsProcessor = MarketsProcessor(parser, calculateSparklines) private val orderbookProcessor = OrderbookProcessor(parser) private val candlesProcessor = CandlesProcessor(parser) + private val tradesProcessor = TradesProcessorV2(TradeProcessorV2(parser, localizer)) internal var groupingMultiplier: Int get() = if (staticTyping) orderbookProcessor.groupingMultiplier else marketsProcessor.groupingMultiplier @@ -151,6 +155,32 @@ internal class MarketsSummaryProcessor( return existing } + fun processTradesSubscribed( + existing: InternalMarketSummaryState, + marketId: String, + content: IndexerTradeResponse? + ): InternalMarketSummaryState { + val marketState = existing.markets[marketId] ?: InternalMarketState() + existing.markets[marketId] = tradesProcessor.processSubscribed( + existing = marketState, + payload = content, + ) + return existing + } + + fun processTradesUpdates( + existing: InternalMarketSummaryState, + marketId: String, + content: IndexerTradeResponse? + ): InternalMarketSummaryState { + val marketState = existing.markets[marketId] ?: InternalMarketState() + existing.markets[marketId] = tradesProcessor.processChannelData( + existing = marketState, + payload = content, + ) + return existing + } + internal fun subscribedDeprecated( existing: Map?, content: Map diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/TradeProcessorV2.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/TradesProcessorV2.kt similarity index 74% rename from src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/TradeProcessorV2.kt rename to src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/TradesProcessorV2.kt index 338bd0de4..6dd42cd54 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/TradeProcessorV2.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/TradesProcessorV2.kt @@ -7,45 +7,49 @@ import exchange.dydx.abacus.output.input.OrderType import exchange.dydx.abacus.processor.base.mergeWithIds import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol +import exchange.dydx.abacus.state.internalstate.InternalMarketState import exchange.dydx.abacus.utils.IndexerResponseParsingException import exchange.dydx.abacus.utils.Logger import exchange.dydx.abacus.utils.parseException +import indexer.codegen.IndexerTradeResponse import indexer.codegen.IndexerTradeResponseObject -import kotlinx.serialization.Serializable - -@Serializable -data class TradesResponse( - val trades: List -) internal class TradesProcessorV2( private val tradeProcessor: TradeProcessorV2, private val limit: Int = TRADES_LIMIT, ) { fun processSubscribed( - payload: TradesResponse - ): List { - return payload.trades.mapNotNull { + existing: InternalMarketState, + payload: IndexerTradeResponse? + ): InternalMarketState { + existing.trades = payload?.trades?.mapNotNull { tradeProcessor.process(it) } + return existing } fun processChannelData( - existing: List?, - payload: TradesResponse, - ): List { - val new = payload.trades.mapNotNull { + existing: InternalMarketState, + payload: IndexerTradeResponse?, + ): InternalMarketState { + val new = payload?.trades?.mapNotNull { tradeProcessor.process(it) } - val merged = existing?.let { - mergeWithIds(new, existing) { trade -> trade.id } - } ?: new + val existingTrades = existing.trades + val merged = + if (new != null && existingTrades != null) { + mergeWithIds(new, existingTrades) { trade -> trade.id } + } else { + new ?: existingTrades ?: emptyList() + } - return if (merged.size > limit) { + existing.trades = if (merged.size > limit) { merged.subList(0, limit) } else { merged } + + return existing } } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Candles.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Candles.kt index 4c0b393dc..b15fc5f77 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Candles.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Candles.kt @@ -88,7 +88,6 @@ internal fun TradingStateMachine.receivedCandles( ): StateChanges { if (staticTyping) { val candlesPayload = parser.asTypedObject(payload) - print(candlesPayload) marketsProcessor.processCandles( existing = internalState.marketsSummary, marketId = marketId, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Trades.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Trades.kt index 42e416aac..4e4dab3b0 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Trades.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Trades.kt @@ -1,11 +1,10 @@ package exchange.dydx.abacus.state.model -import exchange.dydx.abacus.processor.markets.TradesResponse import exchange.dydx.abacus.protocols.asTypedObject import exchange.dydx.abacus.state.changes.Changes import exchange.dydx.abacus.state.changes.StateChanges -import exchange.dydx.abacus.state.internalstate.InternalMarketState import exchange.dydx.abacus.utils.toJson +import indexer.codegen.IndexerTradeResponse import kollections.iListOf internal fun TradingStateMachine.receivedTrades( @@ -13,17 +12,15 @@ internal fun TradingStateMachine.receivedTrades( payload: Map ): StateChanges? { return if (market != null) { - this.marketsSummary = marketsProcessor.receivedTradesDeprecated(marketsSummary, market, payload) if (staticTyping) { - val marketState = internalState.marketsSummary.markets[market] - val response = parser.asTypedObject(payload.toJson()) - val trades = response?.let { tradesProcessorV2.processSubscribed(it) } ?: emptyList() - - if (marketState != null) { - marketState.trades = trades - } else { - internalState.marketsSummary.markets[market] = InternalMarketState(trades) - } + val response = parser.asTypedObject(payload.toJson()) + marketsProcessor.processTradesSubscribed( + existing = internalState.marketsSummary, + marketId = market, + content = response, + ) + } else { + this.marketsSummary = marketsProcessor.receivedTradesDeprecated(marketsSummary, market, payload) } StateChanges(iListOf(Changes.trades), iListOf(market)) } else { @@ -36,9 +33,15 @@ internal fun TradingStateMachine.receivedTradesChanges( payload: Map ): StateChanges? { return if (market != null) { - this.marketsSummary = marketsProcessor.receivedTradesChangesDeprecated(marketsSummary, market, payload) if (staticTyping) { - processTradeUpdates(market, payload) + val response = parser.asTypedObject(payload.toJson()) + marketsProcessor.processTradesUpdates( + existing = internalState.marketsSummary, + marketId = market, + content = response, + ) + } else { + this.marketsSummary = marketsProcessor.receivedTradesChangesDeprecated(marketsSummary, market, payload) } StateChanges(iListOf(Changes.trades), iListOf(market)) } else { @@ -51,31 +54,20 @@ internal fun TradingStateMachine.receivedBatchedTradesChanges( payload: List ): StateChanges? { return if (market != null) { - this.marketsSummary = marketsProcessor.receivedBatchedTradesChanges(marketsSummary, market, payload) if (staticTyping) { payload.mapNotNull { parser.asNativeMap(it) }.forEach { tradeUpdatePayload -> - processTradeUpdates(market, tradeUpdatePayload) + val response = parser.asTypedObject(tradeUpdatePayload.toJson()) + marketsProcessor.processTradesUpdates( + existing = internalState.marketsSummary, + marketId = market, + content = response, + ) } + } else { + this.marketsSummary = marketsProcessor.receivedBatchedTradesChanges(marketsSummary, market, payload) } StateChanges(iListOf(Changes.trades), iListOf(market)) } else { null } } - -private fun TradingStateMachine.processTradeUpdates( - market: String, - payload: Map, -) { - val marketState = internalState.marketsSummary.markets[market] - val response = parser.asTypedObject(payload.toJson()) - val trades = response?.let { - tradesProcessorV2.processChannelData(marketState?.trades, it) - } ?: emptyList() - - if (marketState != null) { - marketState.trades = trades - } else { - internalState.marketsSummary.markets[market] = InternalMarketState(trades) - } -} diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt index 1b2fa40e1..76d3cc1cb 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt @@ -36,8 +36,6 @@ import exchange.dydx.abacus.processor.configs.ConfigsProcessor import exchange.dydx.abacus.processor.configs.RewardsParamsProcessor import exchange.dydx.abacus.processor.launchIncentive.LaunchIncentiveProcessor import exchange.dydx.abacus.processor.markets.MarketsSummaryProcessor -import exchange.dydx.abacus.processor.markets.TradeProcessorV2 -import exchange.dydx.abacus.processor.markets.TradesProcessorV2 import exchange.dydx.abacus.processor.router.IRouterProcessor import exchange.dydx.abacus.processor.router.skip.SkipProcessor import exchange.dydx.abacus.processor.router.squid.SquidProcessor @@ -102,9 +100,9 @@ open class TradingStateMachine( internal val parser: ParserProtocol = Parser() internal val marketsProcessor = MarketsSummaryProcessor( parser = parser, + localizer = localizer, staticTyping = staticTyping, ) - internal val tradesProcessorV2 = TradesProcessorV2(TradeProcessorV2(parser, localizer)) internal val assetsProcessor = run { val processor = AssetsProcessor( parser = parser, @@ -1142,15 +1140,20 @@ open class TradingStateMachine( if (markets != null) { val modified = trades?.toIMutableMap() ?: mutableMapOf() for (marketId in markets) { - val data = parser.asList( - parser.value( - data, - "markets.markets.$marketId.trades", - ), - ) as? IList> - val existing = trades?.get(marketId) - val trades = MarketTrade.create(existing, parser, data, localizer) - modified.typedSafeSet(marketId, trades) + if (staticTyping) { + val trades = internalState.marketsSummary.markets[marketId]?.trades + modified.typedSafeSet(marketId, trades?.toIList()) + } else { + val data = parser.asList( + parser.value( + data, + "markets.markets.$marketId.trades", + ), + ) as? IList> + val existing = trades?.get(marketId) + val trades = MarketTrade.create(existing, parser, data, localizer) + modified.typedSafeSet(marketId, trades) + } } trades = modified } else { diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/payload/BaseTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/payload/BaseTests.kt index 4fab80d0d..8ce4ebcb2 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/payload/BaseTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/payload/BaseTests.kt @@ -290,11 +290,17 @@ open class BaseTests( state?.historicalFundings, "historicalFundings", ) - verifyMarketsTradesState( - parser.asNativeMap(perp.marketsSummary?.get("markets")), - state?.trades, - "trades", - ) + if (staticTyping) { + for ((key, value) in perp.internalState.marketsSummary.markets) { + assertEquals(value.trades, state?.trades?.get(key)) + } + } else { + verifyMarketsTradesState( + parser.asNativeMap(perp.marketsSummary?.get("markets")), + state?.trades, + "trades", + ) + } verifyMarketsCandlesState( parser.asNativeMap(perp.marketsSummary?.get("markets")), state?.candles, diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v3/V3MarketsDelayedTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v3/V3MarketsDelayedTests.kt index 191ff1755..918ecac2f 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v3/V3MarketsDelayedTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v3/V3MarketsDelayedTests.kt @@ -4,6 +4,7 @@ import exchange.dydx.abacus.tests.extensions.loadTrades import kotlin.test.Test import kotlin.test.assertNotNull import kotlin.test.assertNull +import kotlin.test.assertTrue class V3MarketsDelayedTests : V3BaseTests() { @Test @@ -28,11 +29,16 @@ class V3MarketsDelayedTests : V3BaseTests() { assertNull(perp.state?.marketsSummary) }, ) - test( - { - perp.loadTrades(mock) - }, - """ + + if (perp.staticTyping) { + perp.loadTrades(mock) + assertNull(perp.state?.marketsSummary) + } else { + test( + { + perp.loadTrades(mock) + }, + """ { "markets": { "markets": { @@ -45,11 +51,13 @@ class V3MarketsDelayedTests : V3BaseTests() { } } } - """.trimIndent(), - { - assertNull(perp.state?.marketsSummary) - }, - ) + """.trimIndent(), + { + assertNull(perp.state?.marketsSummary) + }, + ) + } + test( { loadMarkets() @@ -74,11 +82,17 @@ class V3MarketsDelayedTests : V3BaseTests() { @Test fun testTradesFirst() { - test( - { - perp.loadTrades(mock) - }, - """ + if (perp.staticTyping) { + perp.loadTrades(mock) + val market = perp.internalState.marketsSummary.markets.get("ETH-USD") + assertTrue { market?.trades?.isNotEmpty() == true } + assertNull(perp.state?.marketsSummary) + } else { + test( + { + perp.loadTrades(mock) + }, + """ { "markets": { "markets": { @@ -89,16 +103,24 @@ class V3MarketsDelayedTests : V3BaseTests() { } } } - """.trimIndent(), - { - assertNull(perp.state?.marketsSummary) - }, - ) - test( - { - loadOrderbook() - }, - """ + """.trimIndent(), + { + assertNull(perp.state?.marketsSummary) + }, + ) + } + + if (perp.staticTyping) { + loadOrderbook() + val market = perp.internalState.marketsSummary.markets.get("ETH-USD") + assertTrue { market?.trades?.isNotEmpty() == true } + assertNull(perp.state?.marketsSummary) + } else { + test( + { + loadOrderbook() + }, + """ { "markets": { "markets": { @@ -111,16 +133,24 @@ class V3MarketsDelayedTests : V3BaseTests() { } } } - """.trimIndent(), - { - assertNull(perp.state?.marketsSummary) - }, - ) - test( - { - loadMarkets() - }, - """ + """.trimIndent(), + { + assertNull(perp.state?.marketsSummary) + }, + ) + } + + if (perp.staticTyping) { + loadMarkets() + val market = perp.internalState.marketsSummary.markets.get("ETH-USD") + assertTrue { market?.trades?.isNotEmpty() == true } + assertNotNull(perp.state?.marketsSummary) + } else { + test( + { + loadMarkets() + }, + """ { "markets": { "markets": { @@ -131,10 +161,11 @@ class V3MarketsDelayedTests : V3BaseTests() { } } } - """.trimIndent(), - { - assertNotNull(perp.state?.marketsSummary) - }, - ) + """.trimIndent(), + { + assertNotNull(perp.state?.marketsSummary) + }, + ) + } } } diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4DuplicateWebsocketMessageTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4DuplicateWebsocketMessageTests.kt index fdc563038..5768bc84a 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4DuplicateWebsocketMessageTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4DuplicateWebsocketMessageTests.kt @@ -2,6 +2,7 @@ package exchange.dydx.abacus.payload.v4 import exchange.dydx.abacus.tests.extensions.loadv4TradesChanged import kotlin.test.Test +import kotlin.test.assertEquals class V4DuplicateWebsocketMessageTests : V4BaseTests() { @@ -80,11 +81,20 @@ class V4DuplicateWebsocketMessageTests : V4BaseTests() { setup() repeat(2) { - test( - { - perp.loadv4TradesChanged(mock, testWsUrl) - }, - """ + if (perp.staticTyping) { + perp.loadv4TradesChanged(mock, testWsUrl) + val market = perp.internalState.marketsSummary.markets.get("ETH-USD") + assertEquals(1, market?.trades?.size) + val firstItem = market?.trades?.get(0) + assertEquals("8ee6d90d-272d-5edd-bf0f-2e4d6ae3d3b7", firstItem?.id) + assertEquals("BUY", firstItem?.side?.rawValue) + assertEquals(1.593707, firstItem?.size) + } else { + test( + { + perp.loadv4TradesChanged(mock, testWsUrl) + }, + """ { "markets":{ "markets":{ @@ -104,8 +114,9 @@ class V4DuplicateWebsocketMessageTests : V4BaseTests() { } } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } } } diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4MarketsTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4MarketsTests.kt index 389430f0f..b9e51d072 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4MarketsTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4MarketsTests.kt @@ -455,11 +455,20 @@ class V4MarketsTests : V4BaseTests() { } private fun testTradesSubscribed() { - test( - { - perp.loadv4TradesSubscribed(mock, testWsUrl) - }, - """ + if (perp.staticTyping) { + perp.loadv4TradesSubscribed(mock, testWsUrl) + val trades = perp.state?.trades?.get("ETH-USD") + assertEquals(trades?.size, 100) + val firstItem = trades?.firstOrNull() + assertEquals(firstItem?.side?.rawValue, "SELL") + assertEquals(firstItem?.size, 9.5E-4) + assertEquals(firstItem?.price, 1255.98) + } else { + test( + { + perp.loadv4TradesSubscribed(mock, testWsUrl) + }, + """ { "markets":{ "markets":{ @@ -479,24 +488,34 @@ class V4MarketsTests : V4BaseTests() { } } } - """.trimIndent(), - { - val trades = - parser.asList(parser.value(perp.data, "markets.markets.ETH-USD.trades")) - assertEquals( - 100, - trades?.size, - ) - }, - ) + """.trimIndent(), + { + val trades = + parser.asList(parser.value(perp.data, "markets.markets.ETH-USD.trades")) + assertEquals( + 100, + trades?.size, + ) + }, + ) + } } private fun testTradesChanged() { - test( - { - perp.loadv4TradesChanged(mock, testWsUrl) - }, - """ + if (perp.staticTyping) { + perp.loadv4TradesChanged(mock, testWsUrl) + val trades = perp.state?.trades?.get("ETH-USD") + assertEquals(trades?.size, 101) + val firstItem = trades?.firstOrNull() + assertEquals(firstItem?.side?.rawValue, "BUY") + assertEquals(firstItem?.size, 1.593707) + assertEquals(firstItem?.price, 1255.949) + } else { + test( + { + perp.loadv4TradesChanged(mock, testWsUrl) + }, + """ { "markets":{ "markets":{ @@ -515,24 +534,34 @@ class V4MarketsTests : V4BaseTests() { } } } - """.trimIndent(), - { - val trades = - parser.asList(parser.value(perp.data, "markets.markets.ETH-USD.trades")) - assertEquals( - 101, - trades?.size, - ) - }, - ) + """.trimIndent(), + { + val trades = + parser.asList(parser.value(perp.data, "markets.markets.ETH-USD.trades")) + assertEquals( + 101, + trades?.size, + ) + }, + ) + } } private fun testTradesBatchChanged() { - test( - { - perp.loadv4TradesBatchChanged(mock, testWsUrl) - }, - """ + if (perp.staticTyping) { + perp.loadv4TradesBatchChanged(mock, testWsUrl) + val trades = perp.state?.trades?.get("ETH-USD") + assertEquals(trades?.size, 240) + val firstItem = trades?.firstOrNull() + assertEquals(firstItem?.side?.rawValue, "SELL") + assertEquals(firstItem?.size, 1.02E-4) + assertEquals(firstItem?.price, 1291.255) + } else { + test( + { + perp.loadv4TradesBatchChanged(mock, testWsUrl) + }, + """ { "markets":{ "markets":{ @@ -551,24 +580,30 @@ class V4MarketsTests : V4BaseTests() { } } } - """.trimIndent(), - { - val trades = - parser.asList(parser.value(perp.data, "markets.markets.ETH-USD.trades")) - assertEquals( - 240, - trades?.size, - ) - }, - ) + """.trimIndent(), + { + val trades = + parser.asList(parser.value(perp.data, "markets.markets.ETH-USD.trades")) + assertEquals( + 240, + trades?.size, + ) + }, + ) + } } private fun testMarketNotOnline() { - test( - { - perp.socket(testWsUrl, mock.marketsChannel.v4_oracle_price_not_in_list, 0, null) - }, - """ + if (perp.staticTyping) { + perp.loadv4MarketsSubscribed(mock, testWsUrl) + val trades = perp.state?.trades?.get("ETH-USD") + assertEquals(trades?.size, 240) + } else { + test( + { + perp.socket(testWsUrl, mock.marketsChannel.v4_oracle_price_not_in_list, 0, null) + }, + """ { "markets":{ "markets":{ @@ -578,16 +613,17 @@ class V4MarketsTests : V4BaseTests() { } } } - """.trimIndent(), - { - val trades = - parser.asList(parser.value(perp.data, "markets.markets.ETH-USD.trades")) - assertEquals( - 240, - trades?.size, - ) - }, - ) + """.trimIndent(), + { + val trades = + parser.asList(parser.value(perp.data, "markets.markets.ETH-USD.trades")) + assertEquals( + 240, + trades?.size, + ) + }, + ) + } } @Test diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/processor/markets/TradesProcessorV2Tests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/processor/markets/TradesProcessorV2Tests.kt index 8095a0f63..f1eb20aa2 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/processor/markets/TradesProcessorV2Tests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/processor/markets/TradesProcessorV2Tests.kt @@ -3,9 +3,11 @@ package exchange.dydx.abacus.processor.markets import exchange.dydx.abacus.output.MarketTrade import exchange.dydx.abacus.output.MarketTradeResources import exchange.dydx.abacus.output.input.OrderSide +import exchange.dydx.abacus.state.internalstate.InternalMarketState import exchange.dydx.abacus.utils.DummyLocalizer import exchange.dydx.abacus.utils.Parser import indexer.codegen.IndexerOrderSide +import indexer.codegen.IndexerTradeResponse import indexer.codegen.IndexerTradeResponseObject import kotlin.test.Test import kotlin.test.assertContentEquals @@ -18,15 +20,16 @@ class TradesProcessorV2Tests { ) @Test fun testSubscribed_happyPath() { - val payload = TradesResponse( - listOf( + val payload = IndexerTradeResponse( + trades = arrayOf( indexerTrade("1"), indexerTrade("2"), indexerTrade("3"), ), ) - val trades = processor.processSubscribed(payload) + val state = InternalMarketState() + processor.processSubscribed(state, payload) assertContentEquals( listOf( @@ -34,13 +37,13 @@ class TradesProcessorV2Tests { expectedMarketTrade("2"), expectedMarketTrade("3"), ), - trades, + state.trades, ) } @Test fun testSubscribed_invalidTrades() { - val payload = TradesResponse( - listOf( + val payload = IndexerTradeResponse( + trades = arrayOf( indexerTrade("1"), IndexerTradeResponseObject( id = "2", @@ -54,32 +57,34 @@ class TradesProcessorV2Tests { ), ) - val trades = processor.processSubscribed(payload) + val state = InternalMarketState() + processor.processSubscribed(state, payload) assertContentEquals( listOf( expectedMarketTrade("1"), ), - trades, + state.trades, ) } @Test fun testChannelData_happyPath() { - val subscribedPayload = TradesResponse( - listOf( + val subscribedPayload = IndexerTradeResponse( + trades = arrayOf( indexerTrade("1"), indexerTrade("2"), ), ) - val initialTrades = processor.processSubscribed(subscribedPayload) - val channelPayload = TradesResponse( - listOf( + val state = InternalMarketState() + processor.processSubscribed(state, subscribedPayload) + val channelPayload = IndexerTradeResponse( + trades = arrayOf( indexerTrade("3"), indexerTrade("4"), ), ) - val finalTrades = processor.processChannelData(initialTrades, channelPayload) + processor.processChannelData(state, channelPayload) assertContentEquals( listOf( @@ -88,20 +93,21 @@ class TradesProcessorV2Tests { expectedMarketTrade("1"), expectedMarketTrade("2"), ), - finalTrades, + state.trades, ) } @Test fun testChannelData_overTradesLimit() { - val subscribedPayload = TradesResponse( - listOf( + val subscribedPayload = IndexerTradeResponse( + trades = arrayOf( indexerTrade("1"), indexerTrade("2"), ), ) - val initialTrades = processor.processSubscribed(subscribedPayload) - val channelPayload = TradesResponse( - listOf( + val state = InternalMarketState() + processor.processSubscribed(state, subscribedPayload) + val channelPayload = IndexerTradeResponse( + trades = arrayOf( indexerTrade("3"), indexerTrade("4"), indexerTrade("5"), @@ -109,7 +115,7 @@ class TradesProcessorV2Tests { ), ) - val finalTrades = processor.processChannelData(initialTrades, channelPayload) + processor.processChannelData(state, channelPayload) assertContentEquals( listOf( @@ -119,26 +125,29 @@ class TradesProcessorV2Tests { expectedMarketTrade("6"), expectedMarketTrade("1"), ), - finalTrades, + state.trades, ) } @Test fun testChannelData_duplicateIds() { - val subscribedPayload = TradesResponse( - listOf( + val subscribedPayload = IndexerTradeResponse( + trades = arrayOf( indexerTrade("1"), indexerTrade("2"), ), ) - val initialTrades = processor.processSubscribed(subscribedPayload) - val channelPayload = TradesResponse( - listOf( + val state = InternalMarketState() + processor.processSubscribed(state, subscribedPayload) + val initialTrades = state.trades + val channelPayload = IndexerTradeResponse( + trades = arrayOf( indexerTrade("1").copy(size = "500.0"), indexerTrade("2").copy(size = "500.0"), ), ) - val finalTrades = processor.processChannelData(initialTrades, channelPayload) + processor.processChannelData(state, channelPayload) + val finalTrades = state.trades assertContentEquals( listOf( From 10cf01e27c93a8b43c25b1c3c710a172b18e7080 Mon Sep 17 00:00:00 2001 From: mobile-build-bot-git Date: Wed, 7 Aug 2024 17:50:50 +0000 Subject: [PATCH 17/63] Bump version --- build.gradle.kts | 2 +- v4_abacus.podspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index a030a5024..3b9cff305 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -52,7 +52,7 @@ allprojects { } group = "exchange.dydx.abacus" -version = "1.8.78" +version = "1.8.80" repositories { google() diff --git a/v4_abacus.podspec b/v4_abacus.podspec index 44c131e45..c3d5f807b 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.78' + spec.version = '1.8.80' spec.homepage = 'https://github.com/dydxprotocol/v4-abacus' spec.source = { :http=> ''} spec.authors = '' From ab3fd0d331ab31c90ff95549a77194cfaf151b2b Mon Sep 17 00:00:00 2001 From: Rui Date: Wed, 7 Aug 2024 17:31:28 -0700 Subject: [PATCH 18/63] WIP --- .../model/TradingStateMachine+TradeInput.kt | 68 +++++++++---------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt index 94535f47d..8d50526ee 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt @@ -217,7 +217,7 @@ fun TradingStateMachine.trade( var sizeChanged = false if (typeText != null) { if (validTradeInput(trade, typeText)) { - var subaccountNumbers = + val subaccountNumbers = MarginCalculator.getChangedSubaccountNumbers( parser, account, @@ -225,8 +225,8 @@ fun TradingStateMachine.trade( trade, ) - when (typeText) { - TradeInputField.type.rawValue, TradeInputField.side.rawValue -> { + when (type) { + TradeInputField.type, TradeInputField.side -> { val text = parser.asString(data) if (text != null) { if (parser.asString(parser.value(trade, "size.input")) == "size.leverage") { @@ -246,7 +246,7 @@ fun TradingStateMachine.trade( } } - TradeInputField.lastInput.rawValue -> { + TradeInputField.lastInput -> { trade.safeSet(typeText, parser.asString(data)) changes = StateChanges( iListOf(Changes.input), @@ -255,10 +255,10 @@ fun TradingStateMachine.trade( ) } - TradeInputField.size.rawValue, - TradeInputField.usdcSize.rawValue, - TradeInputField.leverage.rawValue, - TradeInputField.targetLeverage.rawValue, + TradeInputField.size, + TradeInputField.usdcSize, + TradeInputField.leverage, + TradeInputField.targetLeverage, -> { sizeChanged = (parser.asDouble(data) != parser.asDouble(parser.value(trade, typeText))) @@ -270,13 +270,13 @@ fun TradingStateMachine.trade( ) } - TradeInputField.limitPrice.rawValue, - TradeInputField.triggerPrice.rawValue, - TradeInputField.trailingPercent.rawValue, - TradeInputField.bracketsStopLossPrice.rawValue, - TradeInputField.bracketsStopLossPercent.rawValue, - TradeInputField.bracketsTakeProfitPrice.rawValue, - TradeInputField.bracketsTakeProfitPercent.rawValue, + TradeInputField.limitPrice, + TradeInputField.triggerPrice, + TradeInputField.trailingPercent, + TradeInputField.bracketsStopLossPrice, + TradeInputField.bracketsStopLossPercent, + TradeInputField.bracketsTakeProfitPrice, + TradeInputField.bracketsTakeProfitPercent, -> { trade.safeSet(typeText, parser.asDouble(data)) changes = StateChanges( @@ -286,7 +286,7 @@ fun TradingStateMachine.trade( ) } - TradeInputField.marginMode.rawValue + TradeInputField.marginMode -> { trade.safeSet(typeText, parser.asString(data)) val changedSubaccountNumbers = @@ -303,11 +303,11 @@ fun TradingStateMachine.trade( ) } - TradeInputField.timeInForceType.rawValue, - TradeInputField.goodTilUnit.rawValue, - TradeInputField.bracketsGoodUntilUnit.rawValue, - TradeInputField.execution.rawValue, - TradeInputField.bracketsExecution.rawValue, + TradeInputField.timeInForceType, + TradeInputField.goodTilUnit, + TradeInputField.bracketsGoodUntilUnit, + TradeInputField.execution, + TradeInputField.bracketsExecution, -> { trade.safeSet(typeText, parser.asString(data)) changes = StateChanges( @@ -317,8 +317,8 @@ fun TradingStateMachine.trade( ) } - TradeInputField.goodTilDuration.rawValue, - TradeInputField.bracketsGoodUntilDuration.rawValue, + TradeInputField.goodTilDuration, + TradeInputField.bracketsGoodUntilDuration, -> { trade.safeSet(typeText, parser.asInt(data)) changes = StateChanges( @@ -328,10 +328,10 @@ fun TradingStateMachine.trade( ) } - TradeInputField.reduceOnly.rawValue, - TradeInputField.postOnly.rawValue, - TradeInputField.bracketsStopLossReduceOnly.rawValue, - TradeInputField.bracketsTakeProfitReduceOnly.rawValue, + TradeInputField.reduceOnly, + TradeInputField.postOnly, + TradeInputField.bracketsStopLossReduceOnly, + TradeInputField.bracketsTakeProfitReduceOnly, -> { trade.safeSet(typeText, parser.asBool(data)) changes = StateChanges( @@ -354,10 +354,10 @@ fun TradingStateMachine.trade( ) } if (sizeChanged) { - when (typeText) { - TradeInputField.size.rawValue, - TradeInputField.usdcSize.rawValue, - TradeInputField.leverage.rawValue, + when (type) { + TradeInputField.size, + TradeInputField.usdcSize, + TradeInputField.leverage, -> { trade.safeSet("size.input", typeText) } @@ -374,7 +374,7 @@ fun TradingStateMachine.trade( return StateResponse(state, changes, if (error != null) iListOf(error) else null) } -fun TradingStateMachine.tradeDataOption(typeText: String?): String? { +private fun tradeDataOption(typeText: String?): String? { return when (typeText) { TradeInputField.type.rawValue, TradeInputField.side.rawValue, @@ -414,8 +414,8 @@ fun TradingStateMachine.tradeDataOption(typeText: String?): String? { } } -fun TradingStateMachine.validTradeInput(trade: Map, typeText: String?): Boolean { - val option = this.tradeDataOption(typeText) +private fun TradingStateMachine.validTradeInput(trade: Map, typeText: String?): Boolean { + val option = tradeDataOption(typeText) return if (option != null) { val value = parser.value(trade, option) if (parser.asList(value) != null) { From ea1c620af45a5d79fccda49d318270b2976c6d11 Mon Sep 17 00:00:00 2001 From: Rui Date: Wed, 7 Aug 2024 18:47:00 -0700 Subject: [PATCH 19/63] WIP --- .../calculator/MarginCalculator.kt | 69 +++++++++++- .../calculator/MarginModeCalculator.kt | 6 +- .../processor/input/TradeInputProcessor.kt | 106 ++++++++++++++++++ .../state/internalstate/InternalState.kt | 21 ++++ .../model/TradingStateMachine+TradeInput.kt | 70 ++++-------- 5 files changed, 218 insertions(+), 54 deletions(-) create mode 100644 src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputProcessor.kt diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginCalculator.kt index 18ea731f9..9fbe12a93 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginCalculator.kt @@ -3,9 +3,13 @@ package exchange.dydx.abacus.calculator import abs import exchange.dydx.abacus.output.PerpetualMarket import exchange.dydx.abacus.output.account.Subaccount +import exchange.dydx.abacus.output.account.SubaccountOrder import exchange.dydx.abacus.output.input.MarginMode +import exchange.dydx.abacus.output.input.OrderStatus import exchange.dydx.abacus.output.input.TradeInput import exchange.dydx.abacus.protocols.ParserProtocol +import exchange.dydx.abacus.state.internalstate.InternalAccountState +import exchange.dydx.abacus.state.internalstate.InternalPerpetualPosition import exchange.dydx.abacus.utils.IList import exchange.dydx.abacus.utils.Logger import exchange.dydx.abacus.utils.MAX_LEVERAGE_BUFFER_PERCENT @@ -18,6 +22,21 @@ import kotlin.math.min internal object MarginCalculator { fun findExistingPosition( + account: InternalAccountState, + marketId: String?, + subaccountNumber: Int, + ): InternalPerpetualPosition? { + val position = account.groupedSubaccounts[subaccountNumber]?.openPositions?.get(marketId) + return if ( + (position?.size ?: 0.0) != 0.0 + ) { + position + } else { + null + } + } + + fun findExistingPositionDeprecated( parser: ParserProtocol, account: Map?, marketId: String?, @@ -41,6 +60,18 @@ internal object MarginCalculator { } fun findExistingOrder( + account: InternalAccountState, + marketId: String?, + subaccountNumber: Int, + ): SubaccountOrder? { + val orders = account.groupedSubaccounts[subaccountNumber]?.orders + return orders?.firstOrNull { + it.marketId == marketId && + it.status in listOf(OrderStatus.Open, OrderStatus.Pending, OrderStatus.Untriggered, OrderStatus.PartiallyFilled) + } + } + + fun findExistingOrderDeprecated( parser: ParserProtocol, account: Map?, marketId: String?, @@ -75,17 +106,39 @@ internal object MarginCalculator { } fun findExistingMarginMode( + account: InternalAccountState, + marketId: String?, + subaccountNumber: Int, + ): String? { + val position = findExistingPosition(account, marketId, subaccountNumber) + if (position != null) { + // return if (position.equity != 0.0) "ISOLATED" else "CROSS" + } + + val openOrder = findExistingOrder(account, marketId, subaccountNumber) + return if (openOrder != null) { + if (openOrder.subaccountNumber != subaccountNumber) { + "ISOLATED" + } else { + "CROSS" + } + } else { + null + } + } + + fun findExistingMarginModeDeprecated( parser: ParserProtocol, account: Map?, marketId: String?, subaccountNumber: Int, ): String? { - val position = findExistingPosition(parser, account, marketId, subaccountNumber) + val position = findExistingPositionDeprecated(parser, account, marketId, subaccountNumber) if (position != null) { return if (position["equity"] != null) "ISOLATED" else "CROSS" } - val openOrder = findExistingOrder(parser, account, marketId, subaccountNumber) + val openOrder = findExistingOrderDeprecated(parser, account, marketId, subaccountNumber) if (openOrder != null) { return if (( parser.asInt( @@ -106,6 +159,12 @@ internal object MarginCalculator { } fun findMarketMarginMode( + market: PerpetualMarket?, + ): String { + return market?.configs?.perpetualMarketType?.rawValue ?: "CROSS" + } + + fun findMarketMarginModeDeprecated( parser: ParserProtocol, market: Map?, ): String { @@ -126,11 +185,11 @@ internal object MarginCalculator { ): Boolean { val marketId = parser.asString(market?.get("id")) val existingMarginMode = - findExistingMarginMode(parser, account, marketId, subaccountNumber) + findExistingMarginModeDeprecated(parser, account, marketId, subaccountNumber) return if (existingMarginMode != null) { false } else if (marketId != null) { - findMarketMarginMode(parser, market) == "CROSS" + findMarketMarginModeDeprecated(parser, market) == "CROSS" } else { true } @@ -242,7 +301,7 @@ internal object MarginCalculator { tradeInput: Map? ): Int { val marketId = parser.asString(tradeInput?.get("marketId")) ?: return subaccountNumber - val position = findExistingPosition(parser, account, marketId, subaccountNumber) + val position = findExistingPositionDeprecated(parser, account, marketId, subaccountNumber) return parser.asInt(position?.get("subaccountNumber")) ?: subaccountNumber } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginModeCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginModeCalculator.kt index 4d8448939..1d65090a4 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginModeCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginModeCalculator.kt @@ -16,7 +16,7 @@ object MarginModeCalculator { val modified = tradeInput?.mutable() ?: return null val marketId = parser.asString(tradeInput["marketId"]) val existingMarginMode = - MarginCalculator.findExistingMarginMode( + MarginCalculator.findExistingMarginModeDeprecated( parser, account, marketId, @@ -29,7 +29,7 @@ object MarginModeCalculator { existingMarginMode == "ISOLATED" && parser.asDouble(tradeInput["targetLeverage"]) == null ) { - val existingPosition = MarginCalculator.findExistingPosition( + val existingPosition = MarginCalculator.findExistingPositionDeprecated( parser, account, marketId, @@ -39,7 +39,7 @@ object MarginModeCalculator { modified["targetLeverage"] = existingPositionLeverage ?: 1.0 } } else { - val marketMarginMode = MarginCalculator.findMarketMarginMode( + val marketMarginMode = MarginCalculator.findMarketMarginModeDeprecated( parser, parser.asMap(markets?.get(marketId)), ) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputProcessor.kt new file mode 100644 index 000000000..2262def8f --- /dev/null +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputProcessor.kt @@ -0,0 +1,106 @@ +package exchange.dydx.abacus.processor.input + +import exchange.dydx.abacus.calculator.MarginCalculator +import exchange.dydx.abacus.protocols.ParserProtocol +import exchange.dydx.abacus.state.changes.Changes +import exchange.dydx.abacus.state.changes.StateChanges +import exchange.dydx.abacus.state.internalstate.InternalAccountState +import exchange.dydx.abacus.state.internalstate.InternalInputState +import exchange.dydx.abacus.state.internalstate.InternalInputType +import exchange.dydx.abacus.state.internalstate.InternalMarketState +import exchange.dydx.abacus.state.internalstate.InternalTradeInputState +import kollections.iListOf + +internal class TradeInputProcessor( + private val parser: ParserProtocol, +) { + fun tradeInMarket( + inputState: InternalInputState, + marketState: InternalMarketState, + accountState: InternalAccountState, + marketId: String, + subaccountNumber: Int, + ): StateChanges { + if (inputState.trade.marketId == marketId) { + if (inputState.currentType == InternalInputType.TRADE) { + return StateChanges(iListOf()) // no change + } else { + inputState.currentType = InternalInputType.TRADE + return StateChanges( + changes = iListOf(Changes.input), + markets = null, + subaccountNumbers = iListOf(subaccountNumber), + ) + } + } else { + if (inputState.trade.marketId != null) { + // existing trade + inputState.trade.marketId = marketId + inputState.trade.size = null + inputState.trade.price = null + } else { + // new trade + inputState.trade = initialTradeInputState( + marketId = marketId, + subaccountNumber = subaccountNumber, + accountState = accountState, + marketState = marketState, + ) + } + } + + return StateChanges( + changes = iListOf(Changes.input), + markets = null, + subaccountNumbers = iListOf(subaccountNumber), + ) + } + + private fun initialTradeInputState( + marketId: String?, + subaccountNumber: Int, + accountState: InternalAccountState, + marketState: InternalMarketState, + ): InternalTradeInputState { +// +// val trade = exchange.dydx.abacus.utils.mutableMapOf() +// trade["type"] = "LIMIT" +// trade["side"] = "BUY" +// trade["marketId"] = marketId ?: "ETH-USD" + +// val marginMode = MarginCalculator.findExistingMarginModeDeprecated(parser, account, marketId, subaccountNumber) +// ?: MarginCalculator.findMarketMarginMode(parser, parser.asNativeMap(parser.value(marketsSummary, "markets.$marketId"))) +// +// trade.safeSet("marginMode", marginMode) +// +// val calculator = TradeInputCalculator(parser, TradeCalculation.trade) +// val params = exchange.dydx.abacus.utils.mutableMapOf() +// params.safeSet("markets", parser.asMap(marketsSummary?.get("markets"))) +// params.safeSet("account", account) +// params.safeSet("user", user) +// params.safeSet("trade", trade) +// params.safeSet("rewardsParams", rewardsParams) +// params.safeSet("configs", configs) +// +// val modified = calculator.calculate(params, subaccountNumber, null) +// +// return parser.asMap(modified["trade"])?.mutable() ?: trade + + val marginMode = MarginCalculator.findExistingMarginMode( + account = accountState, + marketId = marketId, + subaccountNumber = subaccountNumber, + ) ?: MarginCalculator.findMarketMarginMode( + market = marketState.perpetualMarket, + ) + + return InternalTradeInputState( + marketId = marketId, + size = null, + price = null, + type = "LIMIT", + side = "BUY", + marginMode = marginMode, // TODO + ) + } +} diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt index 720b6bce4..070207e3b 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt @@ -37,6 +37,27 @@ internal data class InternalState( val launchIncentive: InternalLaunchIncentiveState = InternalLaunchIncentiveState(), val configs: InternalConfigsState = InternalConfigsState(), val marketsSummary: InternalMarketSummaryState = InternalMarketSummaryState(), + val input: InternalInputState = InternalInputState(), +) + +internal enum class InternalInputType { + TRADE, + TRANSFER, +} + +internal data class InternalInputState( + var trade: InternalTradeInputState = InternalTradeInputState(), + var currentType: InternalInputType? = null, +) + +internal data class InternalTradeInputState( + var marketId: String? = null, + var size: Double? = null, + var price: Double? = null, + var type: String? = null, // TODO: enum + var side: String? = null, // TODO: enum + var marginMode: String? = null, // TODO: enum + ) internal data class InternalMarketSummaryState( diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt index 8d50526ee..a5fbfc5e6 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt @@ -53,9 +53,27 @@ enum class TradeInputField(val rawValue: String) { bracketsExecution("brackets.execution"); companion object { - operator fun invoke(rawValue: String) = + operator fun invoke(rawValue: String?) = TradeInputField.values().firstOrNull { it.rawValue == rawValue } } + + internal val tradeDataOption: String? + get() = when (this) { + type, side -> null + size, usdcSize, leverage -> "options.needsSize" + limitPrice -> "options.needsLimitPrice" + triggerPrice -> "options.needsTriggerPrice" + trailingPercent -> "options.needsTrailingPercent" + targetLeverage -> "options.needsTargetLeverage" + goodTilDuration, goodTilUnit -> "options.needsGoodUntil" + reduceOnly -> "options.needsReduceOnly" + postOnly -> "options.needsPostOnly" + bracketsStopLossPrice, bracketsStopLossPercent, bracketsTakeProfitPrice, bracketsTakeProfitPercent, bracketsGoodUntilDuration, bracketsGoodUntilUnit, bracketsStopLossReduceOnly, bracketsTakeProfitReduceOnly, bracketsExecution -> "options.needsBrackets" + timeInForceType -> "options.timeInForceOptions" + execution -> "options.executionOptions" + marginMode -> "options.marginModeOptions" + else -> null + } } internal fun TradingStateMachine.tradeInMarket( @@ -97,13 +115,13 @@ internal fun TradingStateMachine.tradeInMarket( subaccountNumber, ) }.also { - val existingPosition = MarginCalculator.findExistingPosition( + val existingPosition = MarginCalculator.findExistingPositionDeprecated( parser, account, marketId, subaccountNumber, ) - val existingOrder = MarginCalculator.findExistingOrder( + val existingOrder = MarginCalculator.findExistingOrderDeprecated( parser, account, marketId, @@ -158,8 +176,8 @@ private fun TradingStateMachine.initiateTrade( trade["side"] = "BUY" trade["marketId"] = marketId ?: "ETH-USD" - val marginMode = MarginCalculator.findExistingMarginMode(parser, account, marketId, subaccountNumber) - ?: MarginCalculator.findMarketMarginMode(parser, parser.asNativeMap(parser.value(marketsSummary, "markets.$marketId"))) + val marginMode = MarginCalculator.findExistingMarginModeDeprecated(parser, account, marketId, subaccountNumber) + ?: MarginCalculator.findMarketMarginModeDeprecated(parser, parser.asNativeMap(parser.value(marketsSummary, "markets.$marketId"))) trade.safeSet("marginMode", marginMode) @@ -374,48 +392,8 @@ fun TradingStateMachine.trade( return StateResponse(state, changes, if (error != null) iListOf(error) else null) } -private fun tradeDataOption(typeText: String?): String? { - return when (typeText) { - TradeInputField.type.rawValue, - TradeInputField.side.rawValue, - -> null - - TradeInputField.size.rawValue, - TradeInputField.usdcSize.rawValue, - TradeInputField.leverage.rawValue, - -> "options.needsSize" - - TradeInputField.limitPrice.rawValue -> "options.needsLimitPrice" - TradeInputField.triggerPrice.rawValue -> "options.needsTriggerPrice" - TradeInputField.trailingPercent.rawValue -> "options.needsTrailingPercent" - TradeInputField.targetLeverage.rawValue -> "options.needsTargetLeverage" - - TradeInputField.goodTilDuration.rawValue -> "options.needsGoodUntil" - TradeInputField.goodTilUnit.rawValue -> "options.needsGoodUntil" - TradeInputField.reduceOnly.rawValue -> "options.needsReduceOnly" - TradeInputField.postOnly.rawValue -> "options.needsPostOnly" - - TradeInputField.bracketsStopLossPrice.rawValue, - TradeInputField.bracketsStopLossPercent.rawValue, - TradeInputField.bracketsTakeProfitPrice.rawValue, - TradeInputField.bracketsTakeProfitPercent.rawValue, - TradeInputField.bracketsGoodUntilDuration.rawValue, - TradeInputField.bracketsGoodUntilUnit.rawValue, - TradeInputField.bracketsStopLossReduceOnly.rawValue, - TradeInputField.bracketsTakeProfitReduceOnly.rawValue, - TradeInputField.bracketsExecution.rawValue, - -> "options.needsBrackets" - - TradeInputField.timeInForceType.rawValue -> "options.timeInForceOptions" - TradeInputField.execution.rawValue -> "options.executionOptions" - TradeInputField.marginMode.rawValue -> "options.marginModeOptions" - - else -> null - } -} - private fun TradingStateMachine.validTradeInput(trade: Map, typeText: String?): Boolean { - val option = tradeDataOption(typeText) + val option = TradeInputField.invoke(typeText)?.tradeDataOption return if (option != null) { val value = parser.value(trade, option) if (parser.asList(value) != null) { From d46757ab86ff7046c317b6317bb63f563dd5a647 Mon Sep 17 00:00:00 2001 From: Rui Date: Thu, 8 Aug 2024 17:54:55 -0700 Subject: [PATCH 20/63] WIP --- .../calculator/V2/AccountCalculatorV2.kt | 173 ++++++- .../calculator/V2/SubaccountCalculatorV2.kt | 449 ++++++++++++++++++ .../output/account/Subaccount.kt | 144 +++++- .../output/account/SubaccountPosition.kt | 286 ++++++++--- .../wallet/account/AccountProcessor.kt | 2 +- .../wallet/account/SubaccountProcessor.kt | 35 +- .../state/internalstate/InternalState.kt | 70 ++- .../state/model/TradingStateMachine.kt | 40 +- 8 files changed, 1047 insertions(+), 152 deletions(-) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/AccountCalculatorV2.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/AccountCalculatorV2.kt index 5a02f957b..8bdd3e0a8 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/AccountCalculatorV2.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/AccountCalculatorV2.kt @@ -1,7 +1,15 @@ package exchange.dydx.abacus.calculator.v2 +import abs +import exchange.dydx.abacus.calculator.CalculationPeriod +import exchange.dydx.abacus.output.MarketConfigs +import exchange.dydx.abacus.output.input.OrderStatus import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.internalstate.InternalAccountState +import exchange.dydx.abacus.state.internalstate.InternalMarketState +import exchange.dydx.abacus.state.internalstate.InternalMarketSummaryState +import exchange.dydx.abacus.state.internalstate.InternalPendingPositionCalculated +import exchange.dydx.abacus.state.internalstate.InternalPerpetualPendingPosition import exchange.dydx.abacus.state.internalstate.InternalSubaccountState import exchange.dydx.abacus.utils.NUM_PARENT_SUBACCOUNTS import kollections.toIList @@ -9,32 +17,36 @@ import kollections.toIList internal class AccountCalculatorV2( val parser: ParserProtocol, private val useParentSubaccount: Boolean, + private val subaccountCalculator: SubaccountCalculatorV2 = SubaccountCalculatorV2(parser), ) { fun calculate( account: InternalAccountState, subaccountNumbers: List, -// configs: Map?, -// markets: Map?, -// price: Map?, -// periods: Set, + marketsSummary: InternalMarketSummaryState, + price: Map?, + configs: MarketConfigs?, + periods: Set, ): InternalAccountState { for ((subaccountNumber, subaccount) in account.subaccounts) { val parentSubaccountNumber = subaccountNumber % NUM_PARENT_SUBACCOUNTS if (parentSubaccountNumber in subaccountNumbers) { -// account.subaccounts[subaccountNumber] = subaccountCalculator.calculate( -// subaccount, -// configs, -// markets, -// price, -// periods, -// ) + val subAccountState = subaccountCalculator.calculate( + subaccount = subaccount, + marketsSummary = marketsSummary, + price = price, + periods = periods, + configs = configs, + ) + if (subAccountState != null) { + account.subaccounts[subaccountNumber] = subAccountState + } } } if (useParentSubaccount) { return groupSubaccounts( account = account, -// markets = markets, + marketsSummary = marketsSummary, ) } else { return account @@ -43,7 +55,7 @@ internal class AccountCalculatorV2( private fun groupSubaccounts( account: InternalAccountState, -// markets: Map? + marketsSummary: InternalMarketSummaryState, ): InternalAccountState { val subaccounts = account.subaccounts val subaccountNumbers = subaccounts.keys.sorted() @@ -60,12 +72,28 @@ internal class AccountCalculatorV2( ?: subaccounts[parentSubaccountNumber] ?: InternalSubaccountState(subaccountNumber = parentSubaccountNumber) - // TODO: Add other merges + parentSubaccount = mergeChildOpenPositions( + parentSubaccount = parentSubaccount, + childSubaccountNumber = subaccountNumber, + childSubaccount = subaccount, + ) + + parentSubaccount = mergeChildPendingPositions( + parentSubaccount = parentSubaccount, + childSubaccount = subaccount, + markets = marketsSummary.markets, + ) parentSubaccount = mergeOrders( parentSubaccount = parentSubaccount, childSubaccount = subaccount, ) + + parentSubaccount = sumEquity( + parentSubaccount = parentSubaccount, + childSubaccount = subaccount, + ) + groupedSubaccounts[parentSubaccountNumber] = parentSubaccount } } @@ -73,6 +101,109 @@ internal class AccountCalculatorV2( return account } + private fun mergeChildOpenPositions( + parentSubaccount: InternalSubaccountState, + childSubaccountNumber: Int, + childSubaccount: InternalSubaccountState, + ): InternalSubaccountState { + val parentOpenPositions = parentSubaccount.openPositions + val modifiedOpenPositions = parentOpenPositions?.toMutableMap() ?: mutableMapOf() + val childOpenPositions = childSubaccount.openPositions + for ((market, childOpenPosition) in childOpenPositions ?: emptyMap()) { +// modifiedChildOpenPosition?.safeSet( +// "childSubaccountNumber", +// childSubaccountNumber, +// ) + modifiedOpenPositions[market] = childOpenPosition + } + parentSubaccount.childSubaccountOpenPositions = modifiedOpenPositions + + return parentSubaccount + } + + private fun mergeChildPendingPositions( + parentSubaccount: InternalSubaccountState, + childSubaccount: InternalSubaccountState, + markets: Map?, + ): InternalSubaccountState { + data class PendingMarket( + var firstOrderId: String, + var orderCount: Int, + ) + + // Each empty subaccount should have order for one market only + // Just in case it has more than one market, we will create + // two separate pending positions. + val childOpenPositions = childSubaccount.openPositions + val childOrders = childSubaccount.orders + val pendingByMarketId = mutableMapOf() + for (order in childOrders ?: emptyList()) { + val marketId = order.marketId ?: continue + + if (childOpenPositions?.containsKey(marketId) == true) { + val existingPositionCurrentSize = + childOpenPositions.get(marketId)?.calculated?.get(CalculationPeriod.current)?.size + if (existingPositionCurrentSize != null && existingPositionCurrentSize.abs() > 0.0) { + continue + } + } + + val orderStatus = order.status + if (!listOf( + OrderStatus.Open, + OrderStatus.Pending, + OrderStatus.Untriggered, + OrderStatus.PartiallyFilled, + ).contains(orderStatus) + ) { + continue + } + + val pending = pendingByMarketId[marketId] + if (pending == null) { + pendingByMarketId[marketId] = PendingMarket( + firstOrderId = order.id, + orderCount = 1, + ) + } else { + pending.orderCount += 1 + } + } + + val modifiedPendingPositions = mutableListOf() + for ((marketId, pending) in pendingByMarketId) { + val market = markets?.get(marketId) ?: continue + val assetId = market.perpetualMarket?.assetId ?: continue + + val calculated = + mutableMapOf() + for (period in CalculationPeriod.entries) { + val childSubaccountCalculated = childSubaccount.calculated[period] + calculated[period] = InternalPendingPositionCalculated( + quoteBalance = childSubaccountCalculated?.quoteBalance, + freeCollateral = childSubaccountCalculated?.freeCollateral, + equity = childSubaccountCalculated?.equity, + ) + } + val pendingPosition = InternalPerpetualPendingPosition( + assetId = assetId, + marketId = marketId, + firstOrderId = pending.firstOrderId, + orderCount = pending.orderCount, + calculated = calculated, + ) + modifiedPendingPositions.add(pendingPosition) + } + val allPendingPositions = parentSubaccount.pendingPositions ?: emptyList() + parentSubaccount.pendingPositions = allPendingPositions.sortedWith { a, b -> + val aMarketId = a.assetId ?: "" + val bMarketId = b.assetId ?: "" + aMarketId.compareTo(bMarketId) + } + + return parentSubaccount + } + private fun mergeOrders( parentSubaccount: InternalSubaccountState, childSubaccount: InternalSubaccountState, @@ -82,4 +213,18 @@ internal class AccountCalculatorV2( parentSubaccount.orders = (parentOrders + childOrders).toIList() return parentSubaccount } + + private fun sumEquity( + parentSubaccount: InternalSubaccountState, + childSubaccount: InternalSubaccountState, + ): InternalSubaccountState { + for (period in CalculationPeriod.entries) { + val parentEquity = parentSubaccount.calculated[period]?.equity + val childEquity = childSubaccount.calculated[period]?.equity + if (parentEquity != null || childEquity != null) { + parentSubaccount.calculated[period]?.equity = (parentEquity ?: 0.0) + (childEquity ?: 0.0) + } + } + return parentSubaccount + } } 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 ab4f3033d..4338b7574 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/SubaccountCalculatorV2.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/SubaccountCalculatorV2.kt @@ -1,12 +1,58 @@ package exchange.dydx.abacus.calculator.v2 +import abs +import exchange.dydx.abacus.calculator.CalculationPeriod +import exchange.dydx.abacus.output.MarketConfigs import exchange.dydx.abacus.output.account.PositionSide +import exchange.dydx.abacus.output.input.MarginMode import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.internalstate.InternalAssetPositionState +import exchange.dydx.abacus.state.internalstate.InternalMarketState +import exchange.dydx.abacus.state.internalstate.InternalMarketSummaryState +import exchange.dydx.abacus.state.internalstate.InternalPerpetualPosition +import exchange.dydx.abacus.state.internalstate.InternalPositionCalculated +import exchange.dydx.abacus.state.internalstate.InternalSubaccountCalculated +import exchange.dydx.abacus.state.internalstate.InternalSubaccountState +import exchange.dydx.abacus.utils.Numeric +import indexer.codegen.IndexerPerpetualPositionStatus internal class SubaccountCalculatorV2( val parser: ParserProtocol ) { + fun calculate( + subaccount: InternalSubaccountState?, + configs: MarketConfigs?, + marketsSummary: InternalMarketSummaryState, + price: Map?, + periods: Set, + ): InternalSubaccountState? { + if (subaccount == null) return null + + calculatePositionsValues( + subaccount = subaccount, + markets = marketsSummary.markets, + price = price, + periods = periods, + ) + calculateSubaccountEquity( + subaccount = subaccount, + positions = subaccount.openPositions, + periods = periods, + ) + calculatePositionsLeverages( + positions = subaccount.openPositions, + markets = marketsSummary.markets, + subaccount = subaccount, + periods = periods, + ) + calculateSubaccountBuyingPower( + subaccount = subaccount, + configs = configs, + periods = periods, + ) + return subaccount + } + fun calculateQuoteBalance( assetPositions: Map? = null, ): Double? { @@ -23,4 +69,407 @@ internal class SubaccountCalculatorV2( null } } + + private fun calculateSubaccountEquity( + subaccount: InternalSubaccountState, + positions: Map?, + periods: Set, + ) { + for (period in periods) { + val calculated = subaccount.calculated[period] ?: InternalSubaccountCalculated() + subaccount.calculated[period] = calculated + + val quoteBalance = calculated.quoteBalance + if (quoteBalance != null) { + var notionalTotal = Numeric.double.ZERO + var valueTotal = Numeric.double.ZERO + var initialRiskTotal = Numeric.double.ZERO + + for ((key, position) in positions ?: emptyMap()) { + val positionCalculated = position.calculated[period] + notionalTotal += positionCalculated?.notionalTotal ?: Numeric.double.ZERO + valueTotal += positionCalculated?.valueTotal ?: Numeric.double.ZERO + initialRiskTotal += positionCalculated?.initialRiskTotal ?: Numeric.double.ZERO + } + + calculated.valueTotal = valueTotal + calculated.initialRiskTotal = initialRiskTotal + + val equity = valueTotal + quoteBalance + val freeCollateral = equity - initialRiskTotal + + calculated.equity = equity + calculated.freeCollateral = freeCollateral + + if (equity > Numeric.double.ZERO) { + calculated.leverage = notionalTotal / equity + calculated.marginUsage = Numeric.double.ONE - freeCollateral / equity + } else { + calculated.leverage = null + calculated.marginUsage = null + } + } else { + calculated.notionalTotal = null + calculated.valueTotal = null + calculated.initialRiskTotal = null + calculated.equity = null + calculated.freeCollateral = null + calculated.leverage = null + calculated.marginUsage = null + } + } + } + + private fun calculatePositionsLeverages( + positions: Map?, + markets: Map?, + subaccount: InternalSubaccountState?, + periods: Set, + ) { + if (positions.isNullOrEmpty()) { + return + } + + for (period in periods) { + val subaccountCalculated = subaccount?.calculated?.get(period) + val initialRiskTotal = subaccountCalculated?.initialRiskTotal + val equity = subaccountCalculated?.equity + for ((key, position) in positions) { + val positionCalculated = position.calculated[period] + + positionCalculated?.leverage = calculatePositionLeverage( + equity = equity, + notionalValue = positionCalculated?.valueTotal, + ) + + positionCalculated?.liquidationPrice = calculatePositionLiquidationPrice( + equity = equity ?: Numeric.double.ZERO, + marketId = key, + positions = positions, + markets = markets, + period = period, + ) + + positionCalculated?.buyingPower = calculatePositionBuyingPower( + equity = equity, + initialRiskTotal = initialRiskTotal, + imf = positionCalculated?.adjustedImf, + ) + } + } + } + + private fun calculateSubaccountBuyingPower( + subaccount: InternalSubaccountState?, + configs: MarketConfigs?, + periods: Set, + ) { + for (period in periods) { + val calculated = subaccount?.calculated?.get(period) + val quoteBalance = calculated?.quoteBalance + if (quoteBalance != null) { + val equity = calculated.equity ?: Numeric.double.ZERO + val initialRiskTotal = calculated.initialRiskTotal ?: Numeric.double.ZERO + val imf = configs?.initialMarginFraction ?: 0.05 + + calculated.buyingPower = calculateBuyingPower( + equity = equity, + initialRiskTotal = initialRiskTotal, + imf = imf, + ) + } else { + calculated?.buyingPower = null + } + } + } + + private fun calculatePositionBuyingPower( + equity: Double?, + initialRiskTotal: Double?, + imf: Double?, + ): Double? { + return if (equity != null && initialRiskTotal != null && imf != null) { + calculateBuyingPower( + equity = equity, + initialRiskTotal = initialRiskTotal, + imf = imf, + ) + } else { + null + } + } + + private fun calculateBuyingPower( + equity: Double, + initialRiskTotal: Double, + imf: Double, + ): Double { + val buyingPowerFreeCollateral = equity - initialRiskTotal + return buyingPowerFreeCollateral / ( + if (imf > Numeric.double.ZERO) { + imf + } else { + 0.05 + } + ) + } + + private fun calculatePositionLeverage( + equity: Double?, + notionalValue: Double?, + ): Double? { + return if (equity != null && notionalValue != null && equity > Numeric.double.ZERO) { + notionalValue / equity + } else { + null + } + } + + private fun calculatePositionLiquidationPrice( + equity: Double, + marketId: String, + positions: Map?, + markets: Map?, + period: CalculationPeriod, + ): Double? { + val otherPositionsRisk = + calculationOtherPositionsRisk( + positions = positions, + markets = markets, + except = marketId, + period = period, + ) + + val position = positions?.get(marketId) ?: return null + val market = markets?.get(marketId) ?: return null + val calculated = position.calculated.get(period) + val maintenanceMarginFraction = calculated?.adjustedMmf ?: return null + val oraclePrice = market.perpetualMarket?.oraclePrice ?: return null + val size = calculated.size ?: return null + + /* + const liquidationPrice = + side === POSITION_SIDES.LONG + ? otherPositionsRisk + .plus(sizeBN.times(oraclePrice)) + .minus(accountEquity) + .div(sizeBN.minus(sizeBN.times(maintenanceMarginFraction))) + : otherPositionsRisk + .plus(sizeBN.times(oraclePrice)) + .minus(accountEquity) + .div(sizeBN.times(maintenanceMarginFraction).plus(sizeBN)); + */ + val denominator = + if (size > Numeric.double.ZERO) (size - size * maintenanceMarginFraction) else (size + size * maintenanceMarginFraction) + + val liquidationPrice = if (denominator != Numeric.double.ZERO) { + (otherPositionsRisk + size * oraclePrice - equity) / denominator + } else { + null + } + + return liquidationPrice?.takeUnless { it < Numeric.double.ZERO } + } + + private fun calculationOtherPositionsRisk( + positions: Map?, + markets: Map?, + except: String, + period: CalculationPeriod, + ): Double { + var risk = Numeric.double.ZERO + for ((key, position) in positions ?: emptyMap()) { + if (key != except) { + risk += calculatePositionRisk( + position = position, + market = markets?.get(key), + period = period, + ) + } + } + return risk + } + + private fun calculatePositionRisk( + position: InternalPerpetualPosition?, + market: InternalMarketState?, + period: CalculationPeriod, + ): Double { + val maintenanceMarginFraction = position?.calculated?.get(period)?.adjustedImf + val oraclePrice = market?.perpetualMarket?.oraclePrice + val size = position?.calculated?.get(period)?.size + + return if (maintenanceMarginFraction != null && oraclePrice != null && size != null) { + size.abs() * oraclePrice * maintenanceMarginFraction + } else { + Numeric.double.ZERO + } + } + + private fun calculatePositionsValues( + subaccount: InternalSubaccountState, + markets: Map?, + price: Map?, + periods: Set, + ): InternalSubaccountState { + for ((key, position) in subaccount.positions ?: emptyMap()) { + val market = markets?.get(key) + if (market != null) { + calculatePositionValues( + position = position, + market = market, + subaccount = subaccount, + price = parser.asDouble(price?.get(key)), + periods = periods, + ) + } + } + return subaccount + } + + private fun calculatePositionValues( + position: InternalPerpetualPosition, + market: InternalMarketState, + subaccount: InternalSubaccountState, + price: Double?, + periods: Set, + ): InternalPerpetualPosition { + for (period in periods) { + val calculated = position.calculated[period] ?: InternalPositionCalculated() + position.calculated[period] = calculated + + if (period == CalculationPeriod.current) { + calculated.size = position.size + } + val size = calculated.size + val entryPrice = position.entryPrice + val status = position.status + + if (size != null && status != null) { + val realizedPnl = position.realizedPnl + if (realizedPnl != null) { + when (status) { + IndexerPerpetualPositionStatus.CLOSED, IndexerPerpetualPositionStatus.LIQUIDATED -> { + calculated.realizedPnlPercent = null + } + else -> { + if (entryPrice != null) { + val positionEntryValue = (size * entryPrice).abs() + calculated.realizedPnlPercent = if (positionEntryValue > Numeric.double.ZERO) realizedPnl / positionEntryValue else null + } + } + } + } else { + calculated.realizedPnlPercent = null + } + + val marketOraclePrice = market.perpetualMarket?.oraclePrice + val oraclePrice = + if (period == CalculationPeriod.current) { + marketOraclePrice + } else { + price ?: marketOraclePrice + } + + if (oraclePrice != null) { + when (status) { + IndexerPerpetualPositionStatus.CLOSED, IndexerPerpetualPositionStatus.LIQUIDATED -> { + resetCalculated(calculated) + } + else -> { + val configs = market.perpetualMarket?.configs + val valueTotal = size * oraclePrice + calculated.valueTotal = valueTotal + val notional = valueTotal.abs() + calculated.notionalTotal = notional + val adjustedImf = calculatedAdjustedImf(configs) + val adjustedMmf = calculatedAdjustedMmf( + configs = configs, + notional = notional, + ) + val maxLeverage = + if (adjustedImf != Numeric.double.ZERO) Numeric.double.ONE / adjustedImf else null + calculated.adjustedImf = adjustedImf + calculated.adjustedMmf = adjustedMmf + calculated.initialRiskTotal = adjustedImf * notional + calculated.maxLeverage = maxLeverage + + if (entryPrice != null) { + val entryValue = size * entryPrice + val currentValue = size * oraclePrice + val unrealizedPnl = currentValue - entryValue + val unrealizedPnlPercent = + if (entryValue != Numeric.double.ZERO) unrealizedPnl / entryValue.abs() else null + calculated.unrealizedPnl = unrealizedPnl + calculated.unrealizedPnlPercent = unrealizedPnlPercent + } + + val marginMode = position.marginMode + when (marginMode) { + MarginMode.Isolated -> { + val equity = subaccount.equity + calculated.marginValue = equity + } + + MarginMode.Cross -> { + val maintenanceMarginFraction = + configs?.maintenanceMarginFraction ?: Numeric.double.ZERO + calculated.marginValue = maintenanceMarginFraction * notional + } + + else -> { + calculated.marginValue = null + } + } + } + } + } else { + resetCalculated(calculated) + } + } else { + resetCalculated(calculated) + } + } + return position + } + + private fun calculatedAdjustedImf( + configs: MarketConfigs? + ): Double { + return configs?.effectiveInitialMarginFraction ?: Numeric.double.ZERO + } + + private fun calculatedAdjustedMmf( + configs: MarketConfigs?, + notional: Double?, + ): Double { + val maintenanceMarginFraction = configs?.maintenanceMarginFraction ?: Numeric.double.ZERO + val notionalValue = notional ?: Numeric.double.ZERO + return calculateV4MarginFraction(configs, maintenanceMarginFraction, notionalValue) + } + + private fun calculateV4MarginFraction( + configs: MarketConfigs?, + initialMarginFraction: Double, + notional: Double, + ): Double { + return initialMarginFraction + } + + private fun resetCalculated(calculated: InternalPositionCalculated) { + calculated.valueTotal = null + calculated.notionalTotal = null + calculated.adjustedImf = null + calculated.adjustedMmf = null + calculated.initialRiskTotal = null + calculated.maxLeverage = null + calculated.unrealizedPnl = null + calculated.unrealizedPnlPercent = null + calculated.marginValue = null + calculated.realizedPnlPercent = null + calculated.leverage = null + calculated.size = null + calculated.liquidationPrice = null + calculated.buyingPower = null + } } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/output/account/Subaccount.kt b/src/commonMain/kotlin/exchange.dydx.abacus/output/account/Subaccount.kt index 5ed521f71..b3a1ab91a 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/output/account/Subaccount.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/output/account/Subaccount.kt @@ -1,5 +1,6 @@ package exchange.dydx.abacus.output.account +import exchange.dydx.abacus.calculator.CalculationPeriod import exchange.dydx.abacus.output.TradeStatesWithDoubleValues import exchange.dydx.abacus.processor.base.ComparisonOrder import exchange.dydx.abacus.protocols.LocalizerProtocol @@ -57,10 +58,11 @@ data class Subaccount( return null } data?.let { - val positionId = parser.asString(data["positionId"]) - val pnlTotal = parser.asDouble(data["pnlTotal"]) - val pnl24h = parser.asDouble(data["pnl24h"]) - val pnl24hPercent = parser.asDouble(data["pnl24hPercent"]) + val positionId = if (staticTyping) null else parser.asString(data["positionId"]) + val pnlTotal = if (staticTyping) null else parser.asDouble(data["pnlTotal"]) + val pnl24h = if (staticTyping) null else parser.asDouble(data["pnl24h"]) + val pnl24hPercent = + if (staticTyping) null else parser.asDouble(data["pnl24hPercent"]) /* val historicalPnl = (data["historicalPnl"] as? List<*>)?.let { val historicalPnl = iMutableListOf() @@ -76,74 +78,160 @@ data class Subaccount( AccountHistoricalPNLs.fromArray(historicalPnl) } */ - val subaccountNumber = parser.asInt(data["subaccountNumber"]) ?: 0 - val quoteBalance = + + val subaccountNumber = if (staticTyping) { + internalState?.subaccountNumber ?: 0 + } else { + parser.asInt(data["subaccountNumber"]) ?: 0 + } + + val quoteBalance = if (staticTyping) { + TradeStatesWithDoubleValues( + current = internalState?.calculated?.get(CalculationPeriod.current)?.quoteBalance, + postOrder = internalState?.calculated?.get(CalculationPeriod.post)?.quoteBalance, + postAllOrders = internalState?.calculated?.get(CalculationPeriod.settled)?.quoteBalance, + ) + } else { TradeStatesWithDoubleValues.create( existing?.quoteBalance, parser, parser.asMap(data["quoteBalance"]), ) - val notionalTotal = + } + + val notionalTotal = if (staticTyping) { + TradeStatesWithDoubleValues( + current = internalState?.calculated?.get(CalculationPeriod.current)?.notionalTotal, + postOrder = internalState?.calculated?.get(CalculationPeriod.post)?.notionalTotal, + postAllOrders = internalState?.calculated?.get(CalculationPeriod.settled)?.notionalTotal, + ) + } else { TradeStatesWithDoubleValues.create( existing?.notionalTotal, parser, parser.asMap(data["notionalTotal"]), ) - val valueTotal = + } + + val valueTotal = if (staticTyping) { + TradeStatesWithDoubleValues( + current = internalState?.calculated?.get(CalculationPeriod.current)?.valueTotal, + postOrder = internalState?.calculated?.get(CalculationPeriod.post)?.valueTotal, + postAllOrders = internalState?.calculated?.get(CalculationPeriod.settled)?.valueTotal, + ) + } else { TradeStatesWithDoubleValues.create( existing?.valueTotal, parser, parser.asMap(data["valueTotal"]), ) - val initialRiskTotal = + } + + val initialRiskTotal = if (staticTyping) { + TradeStatesWithDoubleValues( + current = internalState?.calculated?.get(CalculationPeriod.current)?.initialRiskTotal, + postOrder = internalState?.calculated?.get(CalculationPeriod.post)?.initialRiskTotal, + postAllOrders = internalState?.calculated?.get(CalculationPeriod.settled)?.initialRiskTotal, + ) + } else { TradeStatesWithDoubleValues.create( existing?.initialRiskTotal, parser, parser.asMap(data["initialRiskTotal"]), ) - val adjustedImf = + } + + val adjustedImf = if (staticTyping) { + // This is not being set at the subaccount level + TradeStatesWithDoubleValues( + current = null, + postOrder = null, + postAllOrders = null, + ) + } else { TradeStatesWithDoubleValues.create( existing?.adjustedImf, parser, parser.asMap(data["adjustedImf"]), ) - val equity = + } + + val equity = if (staticTyping) { + TradeStatesWithDoubleValues( + current = internalState?.calculated?.get(CalculationPeriod.current)?.equity, + postOrder = internalState?.calculated?.get(CalculationPeriod.post)?.equity, + postAllOrders = internalState?.calculated?.get(CalculationPeriod.settled)?.equity, + ) + } else { TradeStatesWithDoubleValues.create( existing?.equity, parser, parser.asMap(data["equity"]), ) - val freeCollateral = + } + + val freeCollateral = if (staticTyping) { + TradeStatesWithDoubleValues( + current = internalState?.calculated?.get(CalculationPeriod.current)?.freeCollateral, + postOrder = internalState?.calculated?.get(CalculationPeriod.post)?.freeCollateral, + postAllOrders = internalState?.calculated?.get(CalculationPeriod.settled)?.freeCollateral, + ) + } else { TradeStatesWithDoubleValues.create( existing?.freeCollateral, parser, parser.asMap(data["freeCollateral"]), ) - val leverage = + } + + val leverage = if (staticTyping) { + TradeStatesWithDoubleValues( + current = internalState?.calculated?.get(CalculationPeriod.current)?.leverage, + postOrder = internalState?.calculated?.get(CalculationPeriod.post)?.leverage, + postAllOrders = internalState?.calculated?.get(CalculationPeriod.settled)?.leverage, + ) + } else { TradeStatesWithDoubleValues.create( existing?.leverage, parser, parser.asMap(data["leverage"]), ) - val marginUsage = + } + + val marginUsage = if (staticTyping) { + TradeStatesWithDoubleValues( + current = internalState?.calculated?.get(CalculationPeriod.current)?.marginUsage, + postOrder = internalState?.calculated?.get(CalculationPeriod.post)?.marginUsage, + postAllOrders = internalState?.calculated?.get(CalculationPeriod.settled)?.marginUsage, + ) + } else { TradeStatesWithDoubleValues.create( existing?.marginUsage, parser, parser.asMap(data["marginUsage"]), ) - val buyingPower = + } + + val buyingPower = if (staticTyping) { + TradeStatesWithDoubleValues( + current = internalState?.calculated?.get(CalculationPeriod.current)?.buyingPower, + postOrder = internalState?.calculated?.get(CalculationPeriod.post)?.buyingPower, + postAllOrders = internalState?.calculated?.get(CalculationPeriod.settled)?.buyingPower, + ) + } else { TradeStatesWithDoubleValues.create( existing?.buyingPower, parser, parser.asMap(data["buyingPower"]), ) + } val openPositions = if (staticTyping) { - openPositions( + createOpenPositions( existing = existing?.openPositions, parser = parser, - data = parser.asMap(data["openPositions"]), - openPositions = internalState?.openPositions, + openPositions = internalState?.groupedOpenPositions, + subaccount = internalState, ) } else { openPositionsDeprecated( @@ -177,7 +265,11 @@ data class Subaccount( )) */ - val marginEnabled = parser.asBool(data["marginEnabled"]) ?: true + val marginEnabled = if (staticTyping) { + internalState?.marginEnabled ?: true + } else { + parser.asBool(data["marginEnabled"]) ?: true + } return if (existing?.subaccountNumber != subaccountNumber || existing.positionId != positionId || @@ -227,20 +319,21 @@ data class Subaccount( return null } - private fun openPositions( + private fun createOpenPositions( existing: IList?, parser: ParserProtocol, - data: Map?, openPositions: Map?, - ): IList? { + subaccount: InternalSubaccountState?, + ): IList { val newEntries: MutableList = mutableListOf() for ((key, value) in openPositions?.entries ?: emptySet()) { val position = SubaccountPosition.create( existing = null, parser = parser, - data = data?.get(key) as? Map, + data = emptyMap(), positionId = key, - internalState = value, + position = value, + subaccount = subaccount, ) if (position != null) { newEntries.add(position) @@ -282,7 +375,8 @@ data class Subaccount( parser = parser, data = it, positionId = null, - internalState = null, + position = null, + subaccount = null, ) } }, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/output/account/SubaccountPosition.kt b/src/commonMain/kotlin/exchange.dydx.abacus/output/account/SubaccountPosition.kt index d1a49aba7..d5ec33fb8 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/output/account/SubaccountPosition.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/output/account/SubaccountPosition.kt @@ -1,11 +1,13 @@ package exchange.dydx.abacus.output.account +import exchange.dydx.abacus.calculator.CalculationPeriod import exchange.dydx.abacus.output.TradeStatesWithDoubleValues import exchange.dydx.abacus.output.TradeStatesWithStringValues import exchange.dydx.abacus.output.input.MarginMode import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.internalstate.InternalPerpetualPosition +import exchange.dydx.abacus.state.internalstate.InternalSubaccountState import exchange.dydx.abacus.utils.IMap import exchange.dydx.abacus.utils.Logger import exchange.dydx.abacus.utils.ParsingHelper @@ -52,25 +54,26 @@ data class SubaccountPosition( parser: ParserProtocol, data: Map?, positionId: String?, // e.g., "ETH-USD" - internalState: InternalPerpetualPosition?, + position: InternalPerpetualPosition?, + subaccount: InternalSubaccountState?, ): SubaccountPosition? { Logger.d { "creating Account Position\n" } data?.let { val id = positionId ?: parser.asString(data["id"]) val assetId = if (positionId != null) ParsingHelper.assetId(id) else parser.asString(data["assetId"]) - val resources = internalState?.resources ?: parser.asMap(data["resources"])?.let { + val resources = position?.resources ?: parser.asMap(data["resources"])?.let { SubaccountPositionResources.create(existing?.resources, parser, it) } if (id != null && assetId != null && resources !== null) { - val childSubaccountNumber = internalState?.subaccountNumber + val childSubaccountNumber = position?.subaccountNumber ?: parser.asInt(data["childSubaccountNumber"]) - val marginMode = internalState?.marginMode + val marginMode = position?.marginMode ?: parser.asString(data["marginMode"])?.let { MarginMode.invoke(it) } - val entryPrice = if (internalState?.entryPrice != null) { + val entryPrice = if (position != null) { TradeStatesWithDoubleValues( - current = internalState.entryPrice, + current = position.entryPrice, postOrder = null, postAllOrders = null, ) @@ -82,17 +85,19 @@ data class SubaccountPosition( ) } - val exitPrice = internalState?.exitPrice + val exitPrice = position?.exitPrice ?: parser.asDouble(data["exitPrice"]) - val createdAtMilliseconds = internalState?.createdAt?.toEpochMilliseconds()?.toDouble() - ?: parser.asDatetime(data["createdAt"])?.toEpochMilliseconds()?.toDouble() - val closedAtMilliseconds = internalState?.closedAt?.toEpochMilliseconds()?.toDouble() + val createdAtMilliseconds = + position?.createdAt?.toEpochMilliseconds()?.toDouble() + ?: parser.asDatetime(data["createdAt"])?.toEpochMilliseconds() + ?.toDouble() + val closedAtMilliseconds = position?.closedAt?.toEpochMilliseconds()?.toDouble() ?: parser.asDatetime(data["closedAt"])?.toEpochMilliseconds()?.toDouble() - val netFunding = internalState?.netFunding ?: parser.asDouble(data["netFunding"]) + val netFunding = position?.netFunding ?: parser.asDouble(data["netFunding"]) - val realizedPnl = if (internalState?.realizedPnl != null) { + val realizedPnl = if (position != null) { TradeStatesWithDoubleValues( - current = internalState.realizedPnl, + current = position.realizedPnl, postOrder = null, postAllOrders = null, ) @@ -104,17 +109,26 @@ data class SubaccountPosition( ) } - val realizedPnlPercent = TradeStatesWithDoubleValues.create( - existing?.realizedPnlPercent, - parser, - parser.asMap(data["realizedPnlPercent"]), - ) + val realizedPnlPercent = + if (position != null) { + TradeStatesWithDoubleValues( + current = position.calculated[CalculationPeriod.current]?.realizedPnlPercent, + postOrder = position.calculated[CalculationPeriod.post]?.realizedPnlPercent, + postAllOrders = position.calculated[CalculationPeriod.settled]?.realizedPnlPercent, + ) + } else { + TradeStatesWithDoubleValues.create( + existing?.realizedPnlPercent, + parser, + parser.asMap(data["realizedPnlPercent"]), + ) + } - val unrealizedPnl = if (internalState?.unrealizedPnl != null) { + val unrealizedPnl = if (position != null) { TradeStatesWithDoubleValues( - current = internalState.unrealizedPnl, - postOrder = null, - postAllOrders = null, + current = position.calculated[CalculationPeriod.current]?.unrealizedPnl, + postOrder = position.calculated[CalculationPeriod.post]?.unrealizedPnl, + postAllOrders = position.calculated[CalculationPeriod.settled]?.unrealizedPnl, ) } else { TradeStatesWithDoubleValues.create( @@ -124,17 +138,25 @@ data class SubaccountPosition( ) } - val unrealizedPnlPercent = TradeStatesWithDoubleValues.create( - existing?.unrealizedPnlPercent, - parser, - parser.asMap(data["unrealizedPnlPercent"]), - ) + val unrealizedPnlPercent = if (position != null) { + TradeStatesWithDoubleValues( + current = position.calculated[CalculationPeriod.current]?.unrealizedPnlPercent, + postOrder = position.calculated[CalculationPeriod.post]?.unrealizedPnlPercent, + postAllOrders = position.calculated[CalculationPeriod.settled]?.unrealizedPnlPercent, + ) + } else { + TradeStatesWithDoubleValues.create( + existing?.unrealizedPnlPercent, + parser, + parser.asMap(data["unrealizedPnlPercent"]), + ) + } - val size = if (internalState?.size != null) { + val size = if (position != null) { TradeStatesWithDoubleValues( - current = internalState.size, - postOrder = null, - postAllOrders = null, + current = position.calculated[CalculationPeriod.current]?.size, + postOrder = position.calculated[CalculationPeriod.post]?.size, + postAllOrders = position.calculated[CalculationPeriod.settled]?.size, ) } else { TradeStatesWithDoubleValues.create( @@ -144,83 +166,201 @@ data class SubaccountPosition( ) } - val notionalTotal = + val notionalTotal = if (position != null) { + TradeStatesWithDoubleValues( + current = position.calculated[CalculationPeriod.current]?.notionalTotal, + postOrder = position.calculated[CalculationPeriod.post]?.notionalTotal, + postAllOrders = position.calculated[CalculationPeriod.settled]?.notionalTotal, + ) + } else { TradeStatesWithDoubleValues.create( existing?.notionalTotal, parser, parser.asMap(data["notionalTotal"]), ) - val valueTotal = + } + + val valueTotal = if (position != null) { + TradeStatesWithDoubleValues( + current = position.calculated[CalculationPeriod.current]?.valueTotal, + postOrder = position.calculated[CalculationPeriod.post]?.valueTotal, + postAllOrders = position.calculated[CalculationPeriod.settled]?.valueTotal, + ) + } else { TradeStatesWithDoubleValues.create( existing?.valueTotal, parser, parser.asMap(data["valueTotal"]), ) - val initialRiskTotal = TradeStatesWithDoubleValues.create( - existing?.initialRiskTotal, - parser, - parser.asMap(data["initialRiskTotal"]), - ) - val adjustedImf = + } + + val initialRiskTotal = if (position != null) { + TradeStatesWithDoubleValues( + current = position.calculated[CalculationPeriod.current]?.initialRiskTotal, + postOrder = position.calculated[CalculationPeriod.post]?.initialRiskTotal, + postAllOrders = position.calculated[CalculationPeriod.settled]?.initialRiskTotal, + ) + } else { + TradeStatesWithDoubleValues.create( + existing?.initialRiskTotal, + parser, + parser.asMap(data["initialRiskTotal"]), + ) + } + + val adjustedImf = if (position != null) { + TradeStatesWithDoubleValues( + current = position.calculated[CalculationPeriod.current]?.adjustedImf, + postOrder = position.calculated[CalculationPeriod.post]?.adjustedImf, + postAllOrders = position.calculated[CalculationPeriod.settled]?.adjustedImf, + ) + } else { TradeStatesWithDoubleValues.create( existing?.adjustedImf, parser, parser.asMap(data["adjustedImf"]), ) - val adjustedMmf = + } + + val adjustedMmf = if (position != null) { + TradeStatesWithDoubleValues( + current = position.calculated[CalculationPeriod.current]?.adjustedMmf, + postOrder = position.calculated[CalculationPeriod.post]?.adjustedMmf, + postAllOrders = position.calculated[CalculationPeriod.settled]?.adjustedMmf, + ) + } else { TradeStatesWithDoubleValues.create( existing?.adjustedMmf, parser, parser.asMap(data["adjustedMmf"]), ) - val leverage = + } + + val leverage = if (position != null) { + TradeStatesWithDoubleValues( + current = position.calculated[CalculationPeriod.current]?.leverage, + postOrder = position.calculated[CalculationPeriod.post]?.leverage, + postAllOrders = position.calculated[CalculationPeriod.settled]?.leverage, + ) + } else { TradeStatesWithDoubleValues.create( existing?.leverage, parser, parser.asMap(data["leverage"]), ) - val maxLeverage = + } + + val maxLeverage = if (position != null) { + TradeStatesWithDoubleValues( + current = position.calculated[CalculationPeriod.current]?.maxLeverage, + postOrder = position.calculated[CalculationPeriod.post]?.maxLeverage, + postAllOrders = position.calculated[CalculationPeriod.settled]?.maxLeverage, + ) + } else { TradeStatesWithDoubleValues.create( existing?.leverage, parser, parser.asMap(data["maxLeverage"]), ) - val buyingPower = + } + + val buyingPower = if (position != null) { + TradeStatesWithDoubleValues( + current = position.calculated[CalculationPeriod.current]?.buyingPower, + postOrder = position.calculated[CalculationPeriod.post]?.buyingPower, + postAllOrders = position.calculated[CalculationPeriod.settled]?.buyingPower, + ) + } else { TradeStatesWithDoubleValues.create( existing?.leverage, parser, parser.asMap(data["buyingPower"]), ) - val liquidationPrice = TradeStatesWithDoubleValues.create( - existing?.liquidationPrice, - parser, - parser.asMap(data["liquidationPrice"]), - ) - val freeCollateral = TradeStatesWithDoubleValues.create( - null, - parser, - parser.asMap(data["freeCollateral"]), - ) - val marginUsage = TradeStatesWithDoubleValues.create( - null, - parser, - parser.asMap(data["marginUsage"]), - ) - val quoteBalance = TradeStatesWithDoubleValues.create( - null, - parser, - parser.asMap(data["quoteBalance"]), - ) - val equity = TradeStatesWithDoubleValues.create( - null, - parser, - parser.asMap(data["equity"]), - ) - val marginValue = TradeStatesWithDoubleValues.create( - null, - parser, - parser.asMap(data["marginValue"]), - ) + } + + val liquidationPrice = if (position != null) { + TradeStatesWithDoubleValues( + current = position.calculated[CalculationPeriod.current]?.liquidationPrice, + postOrder = position.calculated[CalculationPeriod.post]?.liquidationPrice, + postAllOrders = position.calculated[CalculationPeriod.settled]?.liquidationPrice, + ) + } else { + TradeStatesWithDoubleValues.create( + existing?.liquidationPrice, + parser, + parser.asMap(data["liquidationPrice"]), + ) + } + + val freeCollateral = if (subaccount != null) { + TradeStatesWithDoubleValues( + current = subaccount.calculated[CalculationPeriod.current]?.freeCollateral, + postOrder = subaccount.calculated[CalculationPeriod.post]?.freeCollateral, + postAllOrders = subaccount.calculated[CalculationPeriod.settled]?.freeCollateral, + ) + } else { + TradeStatesWithDoubleValues.create( + null, + parser, + parser.asMap(data["freeCollateral"]), + ) + } + + val marginUsage = if (subaccount != null) { + TradeStatesWithDoubleValues( + current = subaccount.calculated[CalculationPeriod.current]?.marginUsage, + postOrder = subaccount.calculated[CalculationPeriod.post]?.marginUsage, + postAllOrders = subaccount.calculated[CalculationPeriod.settled]?.marginUsage, + ) + } else { + TradeStatesWithDoubleValues.create( + null, + parser, + parser.asMap(data["marginUsage"]), + ) + } + + val quoteBalance = if (subaccount != null) { + TradeStatesWithDoubleValues( + current = subaccount.calculated[CalculationPeriod.current]?.quoteBalance, + postOrder = subaccount.calculated[CalculationPeriod.post]?.quoteBalance, + postAllOrders = subaccount.calculated[CalculationPeriod.settled]?.quoteBalance, + ) + } else { + TradeStatesWithDoubleValues.create( + null, + parser, + parser.asMap(data["quoteBalance"]), + ) + } + + val equity = if (subaccount != null) { + TradeStatesWithDoubleValues( + current = subaccount.calculated[CalculationPeriod.current]?.equity, + postOrder = subaccount.calculated[CalculationPeriod.post]?.equity, + postAllOrders = subaccount.calculated[CalculationPeriod.settled]?.equity, + ) + } else { + TradeStatesWithDoubleValues.create( + null, + parser, + parser.asMap(data["equity"]), + ) + } + + val marginValue = if (position != null) { + TradeStatesWithDoubleValues( + current = position.calculated[CalculationPeriod.current]?.marginValue, + postOrder = position.calculated[CalculationPeriod.post]?.marginValue, + postAllOrders = position.calculated[CalculationPeriod.settled]?.marginValue, + ) + } else { + TradeStatesWithDoubleValues.create( + null, + parser, + parser.asMap(data["marginValue"]), + ) + } return if (existing?.id != id || existing.assetId != assetId || diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/wallet/account/AccountProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/wallet/account/AccountProcessor.kt index 643e6146c..8ab88354c 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/wallet/account/AccountProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/wallet/account/AccountProcessor.kt @@ -395,7 +395,7 @@ internal class V4AccountProcessor( internalState: InternalAccountState, content: Map?, ): InternalAccountState { - var modified = internalState + val modified = internalState val subaccounts = parser.asNativeList(parser.value(content, "subaccounts")) subaccountsProcessor.processSubaccounts( internalState = internalState.subaccounts, 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 1e97bc0c6..74e08d327 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 @@ -7,6 +7,7 @@ import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.protocols.asTypedList import exchange.dydx.abacus.protocols.asTypedObject +import exchange.dydx.abacus.state.internalstate.InternalSubaccountCalculated import exchange.dydx.abacus.state.internalstate.InternalSubaccountState import exchange.dydx.abacus.state.manager.BlockAndTime import exchange.dydx.abacus.utils.mutable @@ -116,7 +117,10 @@ internal open class SubaccountProcessor( subaccount = state, payload = assetPositions, ) - state.quoteBalance[CalculationPeriod.current] = subaccountCalculator.calculateQuoteBalance(state.assetPositions) + + val subaccountCalculated = state.calculated[CalculationPeriod.current] ?: InternalSubaccountCalculated() + state.calculated[CalculationPeriod.current] = subaccountCalculated + subaccountCalculated.quoteBalance = subaccountCalculator.calculateQuoteBalance(state.assetPositions) val fills = parser.asTypedList(content["fills"]) state = processFills( @@ -222,29 +226,32 @@ internal open class SubaccountProcessor( return existing } - val modified = existing val subaccountNumber = parser.asInt(payload.subaccountNumber) ?: 0 - modified.subaccountNumber = subaccountNumber - modified.address = payload.address - modified.equity = payload.equity - modified.freeCollateral = payload.freeCollateral - modified.marginEnabled = payload.marginEnabled - modified.updatedAtHeight = payload.updatedAtHeight - modified.latestProcessedBlockHeight = payload.latestProcessedBlockHeight + existing.subaccountNumber = subaccountNumber + existing.address = payload.address + + existing.equity = parser.asDouble(payload.equity) + existing.freeCollateral = parser.asDouble(payload.freeCollateral) + existing.marginEnabled = payload.marginEnabled + existing.updatedAtHeight = payload.updatedAtHeight + existing.latestProcessedBlockHeight = payload.latestProcessedBlockHeight if (firstTime) { - modified.positions = perpetualPositionsProcessor.process( + existing.positions = perpetualPositionsProcessor.process( payload = payload.openPerpetualPositions, ) - modified.assetPositions = assetPositionsProcessor.process( + existing.assetPositions = assetPositionsProcessor.process( payload = payload.assetPositions, ) - modified.quoteBalance[CalculationPeriod.current] = subaccountCalculator.calculateQuoteBalance(modified.assetPositions) - modified.orders = null + val subaccountCalculated = existing.calculated[CalculationPeriod.current] ?: InternalSubaccountCalculated() + existing.calculated[CalculationPeriod.current] = subaccountCalculated + subaccountCalculated.quoteBalance = subaccountCalculator.calculateQuoteBalance(existing.assetPositions) + + existing.orders = null } - return modified + return existing } internal fun receivedDeprecated( diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt index 720b6bce4..354106797 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt @@ -137,22 +137,42 @@ internal data class InternalSubaccountState( var assetPositions: Map? = null, var subaccountNumber: Int, var address: String? = null, - var equity: String? = null, - var freeCollateral: String? = null, + var equity: Double? = null, + var freeCollateral: Double? = null, var marginEnabled: Boolean? = null, var updatedAtHeight: String? = null, var latestProcessedBlockHeight: String? = null, - // Calculate: - var quoteBalance: MutableMap = mutableMapOf(), + var pendingPositions: List? = null, + // for parent subaccount only. This contains the consolidated open positions of all child subaccounts + var childSubaccountOpenPositions: Map? = null, + + // Calculated: + val calculated: MutableMap = mutableMapOf(), ) { + val isParentSubaccount: Boolean + get() = subaccountNumber < NUM_PARENT_SUBACCOUNTS + val openPositions: Map? - get() { - return positions?.filterValues { it.status == IndexerPerpetualPositionStatus.OPEN } - } + get() = positions?.filterValues { it.status == IndexerPerpetualPositionStatus.OPEN } + + val groupedOpenPositions: Map? + get() = if (isParentSubaccount) childSubaccountOpenPositions else openPositions } +internal data class InternalSubaccountCalculated( + var quoteBalance: Double? = null, + var notionalTotal: Double? = null, + var valueTotal: Double? = null, + var initialRiskTotal: Double? = null, + var equity: Double? = null, + var freeCollateral: Double? = null, + var leverage: Double? = null, + var marginUsage: Double? = null, + var buyingPower: Double? = null, +) + internal data class InternalAssetPositionState( val symbol: String? = null, val side: PositionSide? = null, @@ -161,6 +181,22 @@ internal data class InternalAssetPositionState( val subaccountNumber: Int? = null, ) +internal data class InternalPerpetualPendingPosition( + val assetId: String? = null, + val marketId: String? = null, + val firstOrderId: String? = null, + val orderCount: Int? = null, + + // calculated + val calculated: MutableMap = mutableMapOf(), +) + +internal data class InternalPendingPositionCalculated( + val quoteBalance: Double? = null, + val freeCollateral: Double? = null, + val equity: Double? = null, +) + internal data class InternalPerpetualPosition( val market: String? = null, val status: IndexerPerpetualPositionStatus? = null, @@ -179,6 +215,9 @@ internal data class InternalPerpetualPosition( val exitPrice: Double? = null, val subaccountNumber: Int? = null, val resources: SubaccountPositionResources? = null, + + // Calculated: + val calculated: MutableMap = mutableMapOf(), ) { val marginMode: MarginMode? get() { @@ -194,6 +233,23 @@ internal data class InternalPerpetualPosition( } } +internal data class InternalPositionCalculated( + var valueTotal: Double? = null, + var notionalTotal: Double? = null, + var adjustedImf: Double? = null, + var adjustedMmf: Double? = null, + var initialRiskTotal: Double? = null, + var maxLeverage: Double? = null, + var unrealizedPnl: Double? = null, + var unrealizedPnlPercent: Double? = null, + var marginValue: Double? = null, + var realizedPnlPercent: Double? = null, + var leverage: Double? = null, + var size: Double? = null, + var liquidationPrice: Double? = null, + var buyingPower: Double? = null, +) + internal data class InternalAccountBalanceState( val denom: String, val amount: BigDecimal, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt index 76d3cc1cb..c9b8c58f2 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt @@ -903,6 +903,16 @@ open class TradingStateMachine( setOf(CalculationPeriod.current) } + if (staticTyping) { + internalState.wallet.account = accountCalculatorV2.calculate( + account = internalState.wallet.account, + subaccountNumbers = subaccountNumbers, + marketsSummary = internalState.marketsSummary, + periods = periods, + price = null, // priceOverwrite(markets), + configs = null, // This is used to get the IMF.. with "null" the default value 0.05 will be used + ) + } this.marketsSummary?.let { marketsSummary -> parser.asNativeMap(marketsSummary["markets"])?.let { markets -> val modifiedAccount = accountCalculator.calculate( @@ -915,12 +925,6 @@ open class TradingStateMachine( ) this.account = modifiedAccount } - if (staticTyping) { - internalState.wallet.account = accountCalculatorV2.calculate( - account = internalState.wallet.account, - subaccountNumbers = subaccountNumbers, - ) - } } } if (parser.value(account, "groupedSubaccounts") != null) { @@ -1268,13 +1272,13 @@ open class TradingStateMachine( } val subaccountNumbers = changes.subaccountNumbers ?: allSubaccountNumbers() val accountData = this.account - if (accountData != null) { + if (accountData != null || staticTyping) { if (changes.changes.contains(Changes.subaccount)) { account = if (account == null) { Account.create( existing = null, parser = parser, - data = accountData, + data = accountData ?: emptyMap(), tokensInfo = tokensInfo, localizer = localizer, staticTyping = staticTyping, @@ -1308,15 +1312,15 @@ open class TradingStateMachine( } } Account( - account.balances, - account.stakingBalances, - account.stakingDelegations, - account.unbondingDelegation, - account.stakingRewards, - subaccounts, - groupedSubaccounts, - account.tradingRewards, - account.launchIncentivePoints, + balances = account.balances, + stakingBalances = account.stakingBalances, + stakingDelegations = account.stakingDelegations, + unbondingDelegation = account.unbondingDelegation, + stakingRewards = account.stakingRewards, + subaccounts = subaccounts, + groupedSubaccounts = groupedSubaccounts, + tradingRewards = account.tradingRewards, + launchIncentivePoints = account.launchIncentivePoints, ) } } @@ -1327,7 +1331,7 @@ open class TradingStateMachine( account = Account.create( existing = account, parser = parser, - data = accountData, + data = accountData ?: emptyMap(), tokensInfo = tokensInfo, localizer = localizer, staticTyping = staticTyping, From 0c76dfa9885323ab9af6e1ffb8ee6d8be9ec54f8 Mon Sep 17 00:00:00 2001 From: Rui Date: Thu, 8 Aug 2024 18:38:32 -0700 Subject: [PATCH 21/63] Fix tests --- .../calculator/V2/SubaccountCalculatorV2.kt | 2 +- .../kotlin/exchange.dydx.abacus/payload/v4/V4BaseTests.kt | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) 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 4338b7574..09f868615 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/SubaccountCalculatorV2.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/SubaccountCalculatorV2.kt @@ -242,7 +242,7 @@ internal class SubaccountCalculatorV2( val position = positions?.get(marketId) ?: return null val market = markets?.get(marketId) ?: return null - val calculated = position.calculated.get(period) + val calculated = position.calculated[period] val maintenanceMarginFraction = calculated?.adjustedMmf ?: return null val oraclePrice = market.perpetualMarket?.oraclePrice ?: return null val size = calculated.size ?: return null diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4BaseTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4BaseTests.kt index 2807e2a82..cce685e8b 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4BaseTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4BaseTests.kt @@ -141,16 +141,16 @@ open class V4BaseTests(useParentSubaccount: Boolean = false) : BaseTests(127, us trace: String ) { super.verifyAccountState(data, state, staticTyping, obj, trace) - if (data != null) { + if (data != null || (staticTyping && state != null)) { verifyTradingRewardsState( - data = parser.asNativeMap(data["tradingRewards"]), + data = parser.asNativeMap(data?.get("tradingRewards")), obj = obj!!.tradingRewards, staticTyping = staticTyping, state = state?.tradingRewards, trace = "$trace.tradingRewards", ) verifyLaunchIncentivePointsState( - parser.asNativeMap(data["launchIncentivePoints"]), + parser.asNativeMap(data?.get("launchIncentivePoints")), obj.launchIncentivePoints, "$trace.launchIncentivePoints", ) From d954e21a950cf81919e753bd03853f49765e71da Mon Sep 17 00:00:00 2001 From: mobile-build-bot-git Date: Fri, 9 Aug 2024 01:43:08 +0000 Subject: [PATCH 22/63] Bump version --- build.gradle.kts | 2 +- v4_abacus.podspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 3b9cff305..78098bf50 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -52,7 +52,7 @@ allprojects { } group = "exchange.dydx.abacus" -version = "1.8.80" +version = "1.8.83" repositories { google() diff --git a/v4_abacus.podspec b/v4_abacus.podspec index c3d5f807b..1620552e8 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.80' + spec.version = '1.8.83' spec.homepage = 'https://github.com/dydxprotocol/v4-abacus' spec.source = { :http=> ''} spec.authors = '' From 19dcd33e835c145e20f69ac02c7cb5ad4d1ca8f3 Mon Sep 17 00:00:00 2001 From: Rui Date: Thu, 8 Aug 2024 20:37:54 -0700 Subject: [PATCH 23/63] Merge fills and transfers --- .../model/TradingStateMachine+Account.kt | 4 +- ...gStateMachine+AdjustIsolatedMarginInput.kt | 2 +- .../TradingStateMachine+ClosePositionInput.kt | 2 +- .../model/TradingStateMachine+Orderbook.kt | 2 +- .../TradingStateMachine+ParentSubaccount.kt | 81 ++++++++++++++++ .../model/TradingStateMachine+TradeInput.kt | 6 +- .../TradingStateMachine+TransferInput.kt | 2 +- .../TradingStateMachine+TriggerOrdersInput.kt | 2 +- .../state/model/TradingStateMachine.kt | 95 +++++++++++-------- .../state/v2/supervisor/NetworkSupervisor.kt | 2 +- .../payload/v4/V4AccountTests.kt | 22 ++--- .../payload/v4/V4SquidTests.kt | 2 +- .../v4/V4WithdrawalSafetyChecksTests.kt | 4 +- .../TradingStateMachine+TestUtils.kt | 2 +- 14 files changed, 164 insertions(+), 64 deletions(-) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Account.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Account.kt index 95b2a0e2e..56b3c9ac4 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Account.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Account.kt @@ -53,7 +53,7 @@ internal fun TradingStateMachine.updateHeight( ) return if (updated) { val changes = StateChanges(iListOf(Changes.subaccount), null, subaccountIds?.toIList()) - val realChanges = update(changes) + val realChanges = updateStateChanges(changes) StateResponse(state, realChanges, null, null) } else { return StateResponse(state, null, null, null) @@ -66,7 +66,7 @@ internal fun TradingStateMachine.updateHeight( return if (updated) { this.wallet = modifiedWallet val changes = StateChanges(iListOf(Changes.subaccount), null, subaccountIds?.toIList()) - val realChanges = update(changes) + val realChanges = updateStateChanges(changes) StateResponse(state, realChanges, null, null) } else { return StateResponse(state, null, null, null) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+AdjustIsolatedMarginInput.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+AdjustIsolatedMarginInput.kt index 8719ed08a..343d70b5b 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+AdjustIsolatedMarginInput.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+AdjustIsolatedMarginInput.kt @@ -139,7 +139,7 @@ fun TradingStateMachine.adjustIsolatedMargin( input["adjustIsolatedMargin"] = adjustIsolatedMargin this.input = input - changes?.let { update(it) } + changes?.let { updateStateChanges(it) } return StateResponse(state, changes, if (error != null) iListOf(error) else null) } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+ClosePositionInput.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+ClosePositionInput.kt index e5b50ff71..41c1ec140 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+ClosePositionInput.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+ClosePositionInput.kt @@ -113,7 +113,7 @@ fun TradingStateMachine.closePosition( this.input = input changes?.let { - update(it) + updateStateChanges(it) } return StateResponse(state, changes, if (error != null) iListOf(error) else null) } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Orderbook.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Orderbook.kt index 1f2806fb1..1df2dc238 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Orderbook.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Orderbook.kt @@ -110,7 +110,7 @@ internal fun TradingStateMachine.setOrderbookGrouping( ) changes.let { - update(it) + updateStateChanges(it) } return StateResponse(state, changes, null) } else { diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+ParentSubaccount.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+ParentSubaccount.kt index 7996d550c..e68cac443 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+ParentSubaccount.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+ParentSubaccount.kt @@ -1,12 +1,57 @@ package exchange.dydx.abacus.state.model import exchange.dydx.abacus.processor.base.ComparisonOrder +import exchange.dydx.abacus.state.internalstate.InternalAccountState import exchange.dydx.abacus.utils.NUM_PARENT_SUBACCOUNTS import exchange.dydx.abacus.utils.ParsingHelper import exchange.dydx.abacus.utils.mutable import exchange.dydx.abacus.utils.safeSet internal fun TradingStateMachine.mergeFills( + account: InternalAccountState, + subaccountNumbers: List, +): InternalAccountState { + for (subaccountNumber in subaccountNumbers) { + val parentSubaccountNumber = subaccountNumber % NUM_PARENT_SUBACCOUNTS + val groupedFills = account.groupedSubaccounts[parentSubaccountNumber]?.fills ?: emptyList() + val fills = account.subaccounts[subaccountNumber]?.fills ?: emptyList() + val mergedFills = ParsingHelper.mergeTyped( + parser = parser, + existing = groupedFills, + incoming = fills, + comparison = { obj, itemData -> + // sort by fill datetime + val time1 = obj.createdAtMilliseconds + val time2 = itemData.createdAtMilliseconds + var order = ParsingHelper.compare(time1, time2, true) + if (order == ComparisonOrder.same) { + // among fills with the same block + // sort by subaccountNumber + val subaccountNumber1 = obj.subaccountNumber + val subaccountNumber2 = itemData.subaccountNumber + order = ParsingHelper.compare(subaccountNumber1 ?: 0, subaccountNumber2 ?: 0, true) + } + if (order == ComparisonOrder.same) { + // among fills with the same block and same subaccountNumber + // sort by id + val id1 = obj.id + val id2 = itemData.id + order = ParsingHelper.compare(id1, id2, true) + } + order + }, + createObject = { _, obj, itemData -> + itemData + }, + syncItems = false, + ) + + account.groupedSubaccounts[parentSubaccountNumber]?.fills = mergedFills + } + return account +} + +internal fun TradingStateMachine.mergeFillsDeprecated( account: Map?, subaccountNumbers: List, ): Map? { @@ -46,6 +91,42 @@ internal fun TradingStateMachine.mergeFills( } internal fun TradingStateMachine.mergeTransfers( + account: InternalAccountState, + subaccountNumbers: List, +): InternalAccountState { + for (subaccountNumber in subaccountNumbers) { + val parentSubaccountNumber = subaccountNumber % NUM_PARENT_SUBACCOUNTS + val groupedTransfers = account.groupedSubaccounts[parentSubaccountNumber]?.transfers + val transfers = account.subaccounts[subaccountNumber]?.transfers + val mergedTransfers = ParsingHelper.mergeTyped( + parser = parser, + existing = groupedTransfers, + incoming = transfers, + comparison = { obj, itemData -> + // sort by fill datetime + val time1 = obj.updatedAtMilliseconds + val time2 = itemData.updatedAtMilliseconds + var order = ParsingHelper.compare(time1, time2, true) + if (order == ComparisonOrder.same) { + // among fills with the same block + // sort by id + val id1 = obj.id + val id2 = itemData.id + order = ParsingHelper.compare(id1, id2, true) + } + order + }, + createObject = { _, obj, itemData -> + itemData + }, + syncItems = false, + ) + account.groupedSubaccounts[parentSubaccountNumber]?.transfers = mergedTransfers + } + return account +} + +internal fun TradingStateMachine.mergeTransfersDeprecated( account: Map?, subaccountNumbers: List, ): Map? { diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt index 94535f47d..0efc738bd 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt @@ -77,7 +77,7 @@ internal fun TradingStateMachine.tradeInMarket( ) changes.let { - update(it) + updateStateChanges(it) } return StateResponse(state, changes, null) } @@ -143,7 +143,7 @@ internal fun TradingStateMachine.tradeInMarket( ) changes.let { - update(it) + updateStateChanges(it) } return StateResponse(state, changes, null) } @@ -369,7 +369,7 @@ fun TradingStateMachine.trade( this.input = input changes?.let { - update(it) + updateStateChanges(it) } return StateResponse(state, changes, if (error != null) iListOf(error) else null) } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TransferInput.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TransferInput.kt index e9391f86e..5ada0caa3 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TransferInput.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TransferInput.kt @@ -187,7 +187,7 @@ fun TradingStateMachine.transfer( this.input = input changes?.let { - update(it) + updateStateChanges(it) } return StateResponse(state, changes, if (error != null) iListOf(error) else null) } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TriggerOrdersInput.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TriggerOrdersInput.kt index f2940b2a5..69ced24a8 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TriggerOrdersInput.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TriggerOrdersInput.kt @@ -165,7 +165,7 @@ fun TradingStateMachine.triggerOrders( input["triggerOrders"] = triggerOrders this.input = input - changes?.let { update(it) } + changes?.let { updateStateChanges(it) } return StateResponse(state, changes, if (error != null) iListOf(error) else null) } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt index c9b8c58f2..9f9203545 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt @@ -455,7 +455,7 @@ open class TradingStateMachine( } var realChanges = changes changes?.let { - realChanges = update(it) + realChanges = updateStateChanges(it) } return StateResponse(state, realChanges, null, info) } catch (e: ParsingException) { @@ -554,7 +554,7 @@ open class TradingStateMachine( } } if (changes != null) { - update(changes) + updateStateChanges(changes) } val errors = if (error != null) iListOf(error) else null @@ -582,7 +582,7 @@ open class TradingStateMachine( Changes.fundingPayments, ), ) - update(changes) + updateStateChanges(changes) walletProcessor.accountAddress = accountAddress return StateResponse(state, changes, null) } @@ -614,7 +614,7 @@ open class TradingStateMachine( } } - internal fun update(changes: StateChanges): StateChanges { + internal fun updateStateChanges(changes: StateChanges): StateChanges { if (changes.changes.contains(Changes.input)) { val subaccountNumber = changes.subaccountNumbers?.firstOrNull() @@ -674,7 +674,7 @@ open class TradingStateMachine( val wallet = state?.wallet val input = state?.input - state = update(state, changes, tokensInfo, localizer) + state = updateState(state, changes, tokensInfo, localizer) val realChanges = iMutableListOf() for (change in changes.changes) { @@ -927,14 +927,33 @@ open class TradingStateMachine( } } } - if (parser.value(account, "groupedSubaccounts") != null) { - if (changes.changes.contains(Changes.fills)) { - this.account = mergeFills(this.account, subaccountNumbers) + + if (staticTyping) { + if (internalState.wallet.account.groupedSubaccounts.isNotEmpty()) { + if (changes.changes.contains(Changes.fills)) { + internalState.wallet.account = mergeFills( + account = internalState.wallet.account, + subaccountNumbers = subaccountNumbers, + ) + } + if (changes.changes.contains(Changes.transfers) || changes.changes.contains(Changes.fundingPayments)) { + internalState.wallet.account = mergeTransfers( + account = internalState.wallet.account, + subaccountNumbers = subaccountNumbers, + ) + } } - if (changes.changes.contains(Changes.transfers)) { - this.account = mergeTransfers(this.account, subaccountNumbers) + } else { + if (parser.value(account, "groupedSubaccounts") != null) { + if (changes.changes.contains(Changes.fills)) { + this.account = mergeFillsDeprecated(this.account, subaccountNumbers) + } + if (changes.changes.contains(Changes.transfers)) { + this.account = mergeTransfersDeprecated(this.account, subaccountNumbers) + } } } + if (changes.changes.contains(Changes.input)) { val modified = this.input?.mutable() ?: return when (parser.asString(modified["current"])) { @@ -1072,7 +1091,7 @@ open class TradingStateMachine( } } - private fun update( + private fun updateState( state: PerpetualState?, changes: StateChanges, tokensInfo: Map, @@ -1493,26 +1512,26 @@ open class TradingStateMachine( } } return PerpetualState( - assets, - marketsSummary, - orderbooks, - candles, - trades, - historicalFundings, - wallet, - account, - historicalPnl, - fills, - transfers, - fundingPayments, - configs, - input, - subaccountNumbersWithPlaceholders(maxSubaccountNumber()), - transferStatuses, - trackStatuses, - restriction, - launchIncentive, - geo, + assets = assets, + marketsSummary = marketsSummary, + orderbooks = orderbooks, + candles = candles, + trades = trades, + historicalFundings = historicalFundings, + wallet = wallet, + account = account, + historicalPnl = historicalPnl, + fills = fills, + transfers = transfers, + fundingPayments = fundingPayments, + configs = configs, + input = input, + availableSubaccountNumbers = subaccountNumbersWithPlaceholders(maxSubaccountNumber()), + transferStatuses = transferStatuses, + trackStatuses = trackStatuses, + restriction = restriction, + launchIncentive = launchIncentive, + compliance = geo, ) } @@ -1552,7 +1571,7 @@ open class TradingStateMachine( val historicalPnls = state?.historicalPnl?.get("$subaccountNumber") ?: return noChange() val first = historicalPnls.firstOrNull() ?: return noChange() val changes = StateChanges(iListOf(Changes.historicalPnl)) - state = update(state, changes, tokensInfo, localizer) + state = updateState(state, changes, tokensInfo, localizer) StateResponse(state, changes) } else { noChange() @@ -1576,7 +1595,7 @@ open class TradingStateMachine( null, iListOf(subaccountNumber), ) - state = update(state, changes, tokensInfo, localizer) + state = updateState(state, changes, tokensInfo, localizer) StateResponse(state, changes) } else { noChange() @@ -1616,7 +1635,7 @@ open class TradingStateMachine( this.wallet = wallet val changes = StateChanges(iListOf(Changes.subaccount)) - state = update(state, changes, tokensInfo, localizer) + state = updateState(state, changes, tokensInfo, localizer) return StateResponse(state, changes) } } @@ -1632,7 +1651,7 @@ open class TradingStateMachine( error = e.toParsingError() } if (changes != null) { - update(changes) + updateStateChanges(changes) } val errors = if (error != null) iListOf(error) else null @@ -1648,7 +1667,7 @@ open class TradingStateMachine( error = e.toParsingError() } if (changes != null) { - update(changes) + updateStateChanges(changes) } val errors = if (error != null) iListOf(error) else null @@ -1664,7 +1683,7 @@ open class TradingStateMachine( error = e.toParsingError() } if (changes != null) { - update(changes) + updateStateChanges(changes) } val errors = if (error != null) iListOf(error) else null @@ -1673,7 +1692,7 @@ open class TradingStateMachine( fun updateResponse(changes: StateChanges?): StateResponse { if (changes != null) { - update(changes) + updateStateChanges(changes) } return StateResponse(state, changes, null) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/NetworkSupervisor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/NetworkSupervisor.kt index 19ff87cbf..a70c64b82 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/NetworkSupervisor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/NetworkSupervisor.kt @@ -72,7 +72,7 @@ internal open class NetworkSupervisor( if (changes != null) { var realChanges = changes changes.let { - realChanges = stateMachine.update(it) + realChanges = stateMachine.updateStateChanges(it) } if (realChanges != null) { helper.ioImplementations.threading?.async(ThreadingType.main) { 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 e9e6bd5b1..1f55042f4 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4AccountTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4AccountTests.kt @@ -1176,7 +1176,7 @@ class V4AccountTests : V4BaseTests() { fun testAccountBalances() { if (perp.staticTyping) { val changes = perp.onChainAccountBalances(mock.v4OnChainMock.account_balances) - perp.update(changes) + perp.updateStateChanges(changes) assertEquals(perp.internalState.wallet.account.balances?.size, 2) assertEquals( perp.internalState.wallet.account.balances?.get("ibc/8E27BA2D5493AF5636760E354E46004562C46AB7EC0CC4C1CA14E9E20E2545B5"), @@ -1196,7 +1196,7 @@ class V4AccountTests : V4BaseTests() { test( { val changes = perp.onChainAccountBalances(mock.v4OnChainMock.account_balances) - perp.update(changes) + perp.updateStateChanges(changes) return@test StateResponse(perp.state, changes) }, """ @@ -1240,7 +1240,7 @@ class V4AccountTests : V4BaseTests() { test( { val changes = perp.onChainDelegations(mock.v4OnChainMock.account_delegations) - perp.update(changes) + perp.updateStateChanges(changes) return@test StateResponse(perp.state, changes) }, """ @@ -1273,7 +1273,7 @@ class V4AccountTests : V4BaseTests() { payload = mock.historicalTradingRewards.weeklyCall, period = HistoricalTradingRewardsPeriod.WEEKLY, ) - perp.update(changes) + perp.updateStateChanges(changes) assertEquals(perp.internalState.wallet.account.tradingRewards.historical.size, 1) assertEquals( perp.internalState.wallet.account.tradingRewards.historical[HistoricalTradingRewardsPeriod.WEEKLY]?.size, @@ -1284,7 +1284,7 @@ class V4AccountTests : V4BaseTests() { payload = mock.historicalTradingRewards.dailyCall, period = HistoricalTradingRewardsPeriod.DAILY, ) - perp.update(changes) + perp.updateStateChanges(changes) assertEquals(perp.internalState.wallet.account.tradingRewards.historical.size, 2) assertEquals( perp.internalState.wallet.account.tradingRewards.historical[HistoricalTradingRewardsPeriod.DAILY]?.size, @@ -1295,7 +1295,7 @@ class V4AccountTests : V4BaseTests() { payload = mock.historicalTradingRewards.monthlyCall, period = HistoricalTradingRewardsPeriod.MONTHLY, ) - perp.update(changes) + perp.updateStateChanges(changes) assertEquals(perp.internalState.wallet.account.tradingRewards.historical.size, 3) assertEquals( perp.internalState.wallet.account.tradingRewards.historical[HistoricalTradingRewardsPeriod.MONTHLY]?.size, @@ -1306,7 +1306,7 @@ class V4AccountTests : V4BaseTests() { payload = mock.historicalTradingRewards.monthlySecondCall, period = HistoricalTradingRewardsPeriod.MONTHLY, ) - perp.update(changes) + perp.updateStateChanges(changes) assertEquals(perp.internalState.wallet.account.tradingRewards.historical.size, 3) assertEquals( perp.internalState.wallet.account.tradingRewards.historical[HistoricalTradingRewardsPeriod.MONTHLY]?.size, @@ -1319,7 +1319,7 @@ class V4AccountTests : V4BaseTests() { mock.historicalTradingRewards.weeklyCall, HistoricalTradingRewardsPeriod.WEEKLY, ) - perp.update(changes) + perp.updateStateChanges(changes) return@test StateResponse(perp.state, changes) }, """ @@ -1357,7 +1357,7 @@ class V4AccountTests : V4BaseTests() { mock.historicalTradingRewards.dailyCall, HistoricalTradingRewardsPeriod.DAILY, ) - perp.update(changes) + perp.updateStateChanges(changes) return@test StateResponse(perp.state, changes) }, """ @@ -1404,7 +1404,7 @@ class V4AccountTests : V4BaseTests() { mock.historicalTradingRewards.monthlyCall, HistoricalTradingRewardsPeriod.MONTHLY, ) - perp.update(changes) + perp.updateStateChanges(changes) return@test StateResponse(perp.state, changes) }, """ @@ -1458,7 +1458,7 @@ class V4AccountTests : V4BaseTests() { mock.historicalTradingRewards.monthlySecondCall, HistoricalTradingRewardsPeriod.MONTHLY, ) - perp.update(changes) + perp.updateStateChanges(changes) return@test StateResponse(perp.state, changes) }, """ diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4SquidTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4SquidTests.kt index 6e496a96f..05619b29b 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4SquidTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4SquidTests.kt @@ -178,7 +178,7 @@ class V4SquidTests : V4BaseTests() { assertNotNull(stateChange) assertNotNull(perp.data?.get("transferStatuses")) - perp.update(stateChange) + perp.updateStateChanges(stateChange) test({ perp.transfer("DEPOSIT", TransferInputField.type, 0) 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 ecd00f9a1..13c472634 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4WithdrawalSafetyChecksTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4WithdrawalSafetyChecksTests.kt @@ -314,7 +314,7 @@ private fun TradingStateMachine.parseOnChainWithdrawalCapacity(payload: String): error = e.toParsingError() } if (changes != null) { - update(changes) + updateStateChanges(changes) } val errors = if (error != null) iListOf(error) else null @@ -330,7 +330,7 @@ private fun TradingStateMachine.parseOnChainWithdrawalGating(payload: String): S error = e.toParsingError() } if (changes != null) { - update(changes) + updateStateChanges(changes) } val errors = if (error != null) iListOf(error) else null diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/tests/extensions/TradingStateMachine+TestUtils.kt b/src/commonTest/kotlin/exchange.dydx.abacus/tests/extensions/TradingStateMachine+TestUtils.kt index 8bf4dae6e..9cccc0da8 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/tests/extensions/TradingStateMachine+TestUtils.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/tests/extensions/TradingStateMachine+TestUtils.kt @@ -299,7 +299,7 @@ fun TradingStateMachine.parseOnChainEquityTiers(payload: String): StateResponse error = e.toParsingError() } if (changes != null) { - update(changes) + updateStateChanges(changes) } val errors = if (error != null) iListOf(error) else null From 5fc6c608d9a97804b5c0990604507f08807478ea Mon Sep 17 00:00:00 2001 From: Rui Date: Fri, 9 Aug 2024 23:26:35 -0700 Subject: [PATCH 24/63] WIP --- .../calculator/AccountTransformer.kt | 2 +- .../calculator/MarginCalculator.kt | 114 ++++++- .../input/TradeInputField+Actions.kt | 232 +++++++++++++ .../processor/input/TradeInputProcessor.kt | 314 ++++++++++++++++-- .../responses/ParsingError.kt | 7 + .../state/internalstate/InternalState.kt | 85 ++++- ...gStateMachine+AdjustIsolatedMarginInput.kt | 3 +- .../TradingStateMachine+ClosePositionInput.kt | 28 +- .../state/model/TradingStateMachine+Errors.kt | 11 - .../model/TradingStateMachine+Markets.kt | 10 +- .../model/TradingStateMachine+TradeInput.kt | 73 ++-- .../TradingStateMachine+TransferInput.kt | 3 +- .../TradingStateMachine+TriggerOrdersInput.kt | 3 +- .../state/model/TradingStateMachine+Wallet.kt | 2 +- .../state/model/TradingStateMachine.kt | 2 + .../payload/IsolatedMarginModeTests.kt | 2 +- 16 files changed, 806 insertions(+), 85 deletions(-) create mode 100644 src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputField+Actions.kt delete mode 100644 src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Errors.kt diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/AccountTransformer.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/AccountTransformer.kt index 4810c77d2..ec6de5286 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/AccountTransformer.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/AccountTransformer.kt @@ -16,7 +16,7 @@ class AccountTransformer() { ): Map? { val modified = account?.mutable() ?: return null val childSubaccountNumber = - MarginCalculator.getChildSubaccountNumberForIsolatedMarginTrade( + MarginCalculator.getChildSubaccountNumberForIsolatedMarginTradeDeprecated( parser, account, subaccountNumber ?: 0, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginCalculator.kt index 9fbe12a93..1db726d9c 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginCalculator.kt @@ -2,6 +2,7 @@ package exchange.dydx.abacus.calculator import abs import exchange.dydx.abacus.output.PerpetualMarket +import exchange.dydx.abacus.output.PerpetualMarketType import exchange.dydx.abacus.output.account.Subaccount import exchange.dydx.abacus.output.account.SubaccountOrder import exchange.dydx.abacus.output.input.MarginMode @@ -10,6 +11,8 @@ import exchange.dydx.abacus.output.input.TradeInput import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.internalstate.InternalAccountState import exchange.dydx.abacus.state.internalstate.InternalPerpetualPosition +import exchange.dydx.abacus.state.internalstate.InternalSubaccountState +import exchange.dydx.abacus.state.internalstate.InternalTradeInputState import exchange.dydx.abacus.utils.IList import exchange.dydx.abacus.utils.Logger import exchange.dydx.abacus.utils.MAX_LEVERAGE_BUFFER_PERCENT @@ -109,18 +112,19 @@ internal object MarginCalculator { account: InternalAccountState, marketId: String?, subaccountNumber: Int, - ): String? { + ): MarginMode? { val position = findExistingPosition(account, marketId, subaccountNumber) if (position != null) { // return if (position.equity != 0.0) "ISOLATED" else "CROSS" + return position.marginMode } val openOrder = findExistingOrder(account, marketId, subaccountNumber) return if (openOrder != null) { if (openOrder.subaccountNumber != subaccountNumber) { - "ISOLATED" + MarginMode.Isolated } else { - "CROSS" + MarginMode.Cross } } else { null @@ -160,8 +164,13 @@ internal object MarginCalculator { fun findMarketMarginMode( market: PerpetualMarket?, - ): String { - return market?.configs?.perpetualMarketType?.rawValue ?: "CROSS" + ): MarginMode { + val marketType = market?.configs?.perpetualMarketType + return when (marketType) { + PerpetualMarketType.ISOLATED -> return MarginMode.Isolated + PerpetualMarketType.CROSS -> return MarginMode.Cross + else -> MarginMode.Cross + } } fun findMarketMarginModeDeprecated( @@ -195,6 +204,74 @@ internal object MarginCalculator { } } + fun getChildSubaccountNumberForIsolatedMarginTrade( + parser: ParserProtocol, + subaccounts: Map, + subaccountNumber: Int, + marketId: String, + ): Int { + // FE only supports subaccounts that are related to the "main" account (i.e. subaccount 0) and its children + // If there are other utilized subaccounts (e.g. subaccount 1 or 129), ignore them as candidates + val relevantSubaccounts = subaccounts.filterKeys { + key -> parser.asInt(key)?.let { it % NUM_PARENT_SUBACCOUNTS == 0 } ?: false + } + val utilizedSubaccountsMarketIdMap = relevantSubaccounts.mapValues { + val subaccount = it.value + val openPositions = subaccount.openPositions + val openOrders = subaccount.orders?.filter { order -> + val status = order.status + order.status == OrderStatus.Open || status == OrderStatus.Pending || status == OrderStatus.Untriggered || status == OrderStatus.PartiallyFilled + } + + val positionMarketIds = openPositions?.values?.mapNotNull { position -> + position.market + } ?: iListOf() + + val openOrderMarketIds = openOrders?.map { order -> + order.marketId + } ?: iListOf() + + // Return the combined list of marketIds w/o duplicates + (positionMarketIds + openOrderMarketIds).toSet() + } + + // Check if an existing childSubaccount is available to use for Isolated Margin Trade + var availableSubaccountNumber = subaccountNumber + utilizedSubaccountsMarketIdMap.forEach { (key, marketIds) -> + val subaccountNumberToCheck = parser.asInt(key) + if (subaccountNumberToCheck == null) { + Logger.e { "Invalid subaccount number: $key" } + return@forEach + } + if (subaccountNumberToCheck != subaccountNumber) { + if (marketIds.contains(marketId) && marketIds.size <= 1) { + return subaccountNumberToCheck + } else if (marketIds.isEmpty()) { + // Check if subaccount equity is 0 so that funds are moved to a clean account if reclaimUnutilizedChildSubaccountFunds has not been called yet + val equity = subaccounts[subaccountNumberToCheck]?.calculated?.get(CalculationPeriod.current)?.equity ?: 0.0 + if (availableSubaccountNumber == subaccountNumber && equity == 0.0) { + availableSubaccountNumber = subaccountNumberToCheck + } + } + } + } + if (availableSubaccountNumber != subaccountNumber) { + return availableSubaccountNumber + } + + // Find new childSubaccount number available for Isolated Margin Trade + val existingSubaccountNumbers = utilizedSubaccountsMarketIdMap.keys + for (offset in NUM_PARENT_SUBACCOUNTS..MAX_SUBACCOUNT_NUMBER step NUM_PARENT_SUBACCOUNTS) { + val tentativeSubaccountNumber = offset + subaccountNumber + if (!existingSubaccountNumbers.contains(tentativeSubaccountNumber)) { + return tentativeSubaccountNumber + } + } + + // User has reached the maximum number of childSubaccounts for their current parentSubaccount + error("No available subaccount number") + } + /** * @description Get the childSubaccount number that is available for the given marketId * @param parser ParserProtocol @@ -202,7 +279,7 @@ internal object MarginCalculator { * @param subaccountNumber Parent subaccount number * @param tradeInput Trade input data (data.input.trade) */ - fun getChildSubaccountNumberForIsolatedMarginTrade( + fun getChildSubaccountNumberForIsolatedMarginTradeDeprecated( parser: ParserProtocol, account: Map?, subaccountNumber: Int, @@ -281,12 +358,35 @@ internal object MarginCalculator { } fun getChangedSubaccountNumbers( + parser: ParserProtocol, + subaccounts: Map, + subaccountNumber: Int, + tradeInput: InternalTradeInputState? + ): IList { + val marketId = tradeInput?.marketId + if (tradeInput?.marginMode != MarginMode.Isolated || marketId == null) { + return iListOf(subaccountNumber) + } + val childSubaccountNumber = getChildSubaccountNumberForIsolatedMarginTrade( + parser = parser, + subaccounts = subaccounts, + subaccountNumber = subaccountNumber, + marketId = marketId + ) + if (subaccountNumber != childSubaccountNumber) { + return iListOf(subaccountNumber, childSubaccountNumber) + } + + return iListOf(subaccountNumber) + } + + fun getChangedSubaccountNumbersDeprecated( parser: ParserProtocol, account: Map?, subaccountNumber: Int, tradeInput: Map? ): IList { - val childSubaccountNumber = getChildSubaccountNumberForIsolatedMarginTrade(parser, account, subaccountNumber, tradeInput) + val childSubaccountNumber = getChildSubaccountNumberForIsolatedMarginTradeDeprecated(parser, account, subaccountNumber, tradeInput) if (childSubaccountNumber != null && subaccountNumber != childSubaccountNumber) { return iListOf(subaccountNumber, childSubaccountNumber) } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputField+Actions.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputField+Actions.kt new file mode 100644 index 000000000..39a409d00 --- /dev/null +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputField+Actions.kt @@ -0,0 +1,232 @@ +package exchange.dydx.abacus.processor.input + +import exchange.dydx.abacus.output.input.MarginMode +import exchange.dydx.abacus.output.input.OrderSide +import exchange.dydx.abacus.output.input.OrderType +import exchange.dydx.abacus.output.input.TradeInputBracket +import exchange.dydx.abacus.output.input.TradeInputBracketSide +import exchange.dydx.abacus.output.input.TradeInputGoodUntil +import exchange.dydx.abacus.output.input.TradeInputPrice +import exchange.dydx.abacus.output.input.TradeInputSize +import exchange.dydx.abacus.protocols.ParserProtocol +import exchange.dydx.abacus.state.internalstate.InternalTradeInputState +import exchange.dydx.abacus.state.internalstate.safeCreate +import exchange.dydx.abacus.state.model.TradeInputField +import exchange.dydx.abacus.state.model.TradeInputField.bracketsExecution +import exchange.dydx.abacus.state.model.TradeInputField.bracketsGoodUntilDuration +import exchange.dydx.abacus.state.model.TradeInputField.bracketsGoodUntilUnit +import exchange.dydx.abacus.state.model.TradeInputField.bracketsStopLossPercent +import exchange.dydx.abacus.state.model.TradeInputField.bracketsStopLossPrice +import exchange.dydx.abacus.state.model.TradeInputField.bracketsStopLossReduceOnly +import exchange.dydx.abacus.state.model.TradeInputField.bracketsTakeProfitPercent +import exchange.dydx.abacus.state.model.TradeInputField.bracketsTakeProfitPrice +import exchange.dydx.abacus.state.model.TradeInputField.bracketsTakeProfitReduceOnly +import exchange.dydx.abacus.state.model.TradeInputField.execution +import exchange.dydx.abacus.state.model.TradeInputField.goodTilDuration +import exchange.dydx.abacus.state.model.TradeInputField.goodTilUnit +import exchange.dydx.abacus.state.model.TradeInputField.leverage +import exchange.dydx.abacus.state.model.TradeInputField.limitPrice +import exchange.dydx.abacus.state.model.TradeInputField.marginMode +import exchange.dydx.abacus.state.model.TradeInputField.postOnly +import exchange.dydx.abacus.state.model.TradeInputField.reduceOnly +import exchange.dydx.abacus.state.model.TradeInputField.side +import exchange.dydx.abacus.state.model.TradeInputField.size +import exchange.dydx.abacus.state.model.TradeInputField.targetLeverage +import exchange.dydx.abacus.state.model.TradeInputField.timeInForceType +import exchange.dydx.abacus.state.model.TradeInputField.trailingPercent +import exchange.dydx.abacus.state.model.TradeInputField.triggerPrice +import exchange.dydx.abacus.state.model.TradeInputField.type +import exchange.dydx.abacus.state.model.TradeInputField.usdcSize + +// +// Contains the helper functions for each of the trade input fields +// + +// Returns the validation action for the trade input field +internal val TradeInputField.validTradeInputAction: ((InternalTradeInputState) -> Boolean)? + get() = when (this) { + type, side -> null + size, usdcSize, leverage -> { it -> it.options.needsSize } + limitPrice -> { it -> it.options.needsLimitPrice } + triggerPrice -> { it -> it.options.needsTriggerPrice } + trailingPercent -> { it -> it.options.needsTrailingPercent } + targetLeverage -> { it -> it.options.needsTargetLeverage } + goodTilDuration, goodTilUnit -> { it -> it.options.needsGoodUntil } + reduceOnly -> { it -> it.options.needsReduceOnly } + postOnly -> { it -> it.options.needsPostOnly } + bracketsStopLossPrice, + bracketsStopLossPercent, + bracketsTakeProfitPrice, + bracketsTakeProfitPercent, + bracketsGoodUntilDuration, + bracketsGoodUntilUnit, + bracketsStopLossReduceOnly, + bracketsTakeProfitReduceOnly, + bracketsExecution -> { it -> it.options.needsBrackets } + timeInForceType -> { it -> it.options.timeInForceOptions != null } + execution -> { it -> it.options.executionOptions != null } + marginMode -> { it -> it.options.marginModeOptions != null } + TradeInputField.lastInput -> { it -> true } + } + +// Returns the action to read value for the trade input field +internal val TradeInputField.valueAction: ((InternalTradeInputState) -> Any?)? + get() = when (this) { + type -> { it -> it.type } + side -> { it -> it.side } + + marginMode -> { it -> it.marginMode } + targetLeverage -> { it -> it.targetLeverage } + + size -> { it -> it.size?.size } + usdcSize -> { it -> it.size?.usdcSize } + leverage -> { it -> it.size?.leverage } + + TradeInputField.lastInput -> { it -> it.size?.input } + limitPrice -> { it -> it.price?.limitPrice } + triggerPrice -> { it -> it.price?.triggerPrice } + trailingPercent -> { it -> it.price?.trailingPercent } + + timeInForceType -> { it -> it.timeInForce } + goodTilDuration -> { it -> it.goodTil?.duration } + goodTilUnit -> { it -> it.goodTil?.unit } + + execution -> { it -> it.execution } + reduceOnly -> { it -> it.reduceOnly } + postOnly -> { it -> it.postOnly } + + bracketsStopLossPrice -> { it -> it.bracket?.stopLoss?.triggerPrice } + bracketsStopLossPercent -> { it -> it.bracket?.stopLoss?.percent } + bracketsStopLossReduceOnly -> { it -> it.bracket?.stopLoss?.reduceOnly } + bracketsTakeProfitPrice -> { it -> it.bracket?.takeProfit?.triggerPrice } + bracketsTakeProfitPercent -> { it -> it.bracket?.takeProfit?.percent } + bracketsTakeProfitReduceOnly -> { it -> it.bracket?.takeProfit?.reduceOnly } + bracketsGoodUntilDuration -> { it -> it.bracket?.goodTil?.duration } + bracketsGoodUntilUnit -> { it -> it.bracket?.goodTil?.unit } + bracketsExecution -> { it -> it.bracket?.execution } + } + +// Returns the write action to update value for the trade input field +internal val TradeInputField.updateValueAction: ((InternalTradeInputState, String?, ParserProtocol) -> Unit)? + get() = when (this) { + type -> { trade, value, parser -> trade.type = OrderType.invoke(value) } + side -> { trade, value, parser -> trade.side = OrderSide.invoke(value) } + + TradeInputField.lastInput -> { trade, value, parser -> + trade.size = TradeInputSize.safeCreate(trade.size).copy(input = value) + } + + limitPrice -> { trade, value, parser -> + trade.price = + TradeInputPrice.safeCreate(trade.price).copy(limitPrice = parser.asDouble(value)) + } + + triggerPrice -> { trade, value, parser -> + trade.price = + TradeInputPrice.safeCreate(trade.price).copy(triggerPrice = parser.asDouble(value)) + } + + trailingPercent -> { trade, value, parser -> + trade.price = TradeInputPrice.safeCreate(trade.price) + .copy(trailingPercent = parser.asDouble(value)) + } + + bracketsStopLossPrice -> { trade, value, parser -> + val braket = TradeInputBracket.safeCreate(trade.bracket) + val stopLoss = TradeInputBracketSide.safeCreate(braket.stopLoss) + trade.bracket = + braket.copy(stopLoss = stopLoss.copy(triggerPrice = parser.asDouble(value))) + } + + bracketsStopLossPercent -> { trade, value, parser -> + val braket = TradeInputBracket.safeCreate(trade.bracket) + val stopLoss = TradeInputBracketSide.safeCreate(braket.stopLoss) + trade.bracket = braket.copy(stopLoss = stopLoss.copy(percent = parser.asDouble(value))) + } + + bracketsTakeProfitPrice -> { trade, value, parser -> + val braket = TradeInputBracket.safeCreate(trade.bracket) + val takeProfit = TradeInputBracketSide.safeCreate(braket.takeProfit) + trade.bracket = + braket.copy(takeProfit = takeProfit.copy(triggerPrice = parser.asDouble(value))) + } + + bracketsTakeProfitPercent -> { trade, value, parser -> + val braket = TradeInputBracket.safeCreate(trade.bracket) + val takeProfit = TradeInputBracketSide.safeCreate(braket.takeProfit) + trade.bracket = + braket.copy(takeProfit = takeProfit.copy(percent = parser.asDouble(value))) + } + + marginMode -> { trade, value, parser -> + trade.marginMode = MarginMode.invoke(value) + } + + timeInForceType -> { trade, value, parser -> trade.timeInForce = value } + + goodTilUnit -> { trade, value, parser -> + trade.goodTil = TradeInputGoodUntil.safeCreate(trade.goodTil).copy(unit = value) + } + + bracketsGoodUntilUnit -> { trade, value, parser -> + val braket = TradeInputBracket.safeCreate(trade.bracket) + trade.bracket = braket.copy( + goodTil = TradeInputGoodUntil.safeCreate(braket.goodTil).copy(unit = value) + ) + } + + execution -> { trade, value, parser -> trade.execution = value } + + bracketsExecution -> { trade, value, parser -> + trade.bracket = TradeInputBracket.safeCreate(trade.bracket).copy(execution = value) + } + + goodTilDuration -> { trade, value, parser -> + trade.goodTil = TradeInputGoodUntil.safeCreate(trade.goodTil) + .copy(duration = parser.asDouble(value)) + } + + bracketsGoodUntilDuration -> { trade, value, parser -> + val braket = TradeInputBracket.safeCreate(trade.bracket) + trade.bracket = braket.copy( + goodTil = TradeInputGoodUntil.safeCreate(braket.goodTil) + .copy(duration = parser.asDouble(value)) + ) + } + + reduceOnly -> { trade, value, parser -> trade.reduceOnly = parser.asBool(value) ?: false } + + postOnly -> { trade, value, parser -> trade.postOnly = parser.asBool(value) ?: false } + + bracketsStopLossReduceOnly -> { trade, value, parser -> + val braket = TradeInputBracket.safeCreate(trade.bracket) + val stopLoss = TradeInputBracketSide.safeCreate(braket.stopLoss) + trade.bracket = + braket.copy(stopLoss = stopLoss.copy(reduceOnly = parser.asBool(value) ?: false)) + } + + bracketsTakeProfitReduceOnly -> { trade, value, parser -> + val braket = TradeInputBracket.safeCreate(trade.bracket) + val takeProfit = TradeInputBracketSide.safeCreate(braket.takeProfit) + trade.bracket = braket.copy( + takeProfit = takeProfit.copy( + reduceOnly = parser.asBool(value) ?: false + ) + ) + } + + targetLeverage -> { trade, value, parser -> trade.targetLeverage = parser.asDouble(value) } + size -> { trade, value, parser -> + trade.size = TradeInputSize.safeCreate(trade.size).copy(size = parser.asDouble(value)) + } + + usdcSize -> { trade, value, parser -> + trade.size = + TradeInputSize.safeCreate(trade.size).copy(usdcSize = parser.asDouble(value)) + } + + leverage -> { trade, value, parser -> + trade.size = + TradeInputSize.safeCreate(trade.size).copy(leverage = parser.asDouble(value)) + } + } \ No newline at end of file diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputProcessor.kt index 2262def8f..c816398ce 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputProcessor.kt @@ -1,20 +1,82 @@ package exchange.dydx.abacus.processor.input +import abs +import exchange.dydx.abacus.calculator.CalculationPeriod import exchange.dydx.abacus.calculator.MarginCalculator +import exchange.dydx.abacus.calculator.TradeCalculation +import exchange.dydx.abacus.calculator.TradeInputCalculator +import exchange.dydx.abacus.output.PerpetualMarketType +import exchange.dydx.abacus.output.input.MarginMode +import exchange.dydx.abacus.output.input.OrderSide +import exchange.dydx.abacus.output.input.OrderType +import exchange.dydx.abacus.output.input.TradeInputBracket +import exchange.dydx.abacus.output.input.TradeInputBracketSide +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 +import exchange.dydx.abacus.responses.ParsingErrorType +import exchange.dydx.abacus.responses.cannotModify import exchange.dydx.abacus.state.changes.Changes import exchange.dydx.abacus.state.changes.StateChanges import exchange.dydx.abacus.state.internalstate.InternalAccountState import exchange.dydx.abacus.state.internalstate.InternalInputState import exchange.dydx.abacus.state.internalstate.InternalInputType import exchange.dydx.abacus.state.internalstate.InternalMarketState +import exchange.dydx.abacus.state.internalstate.InternalTradeInputOptions import exchange.dydx.abacus.state.internalstate.InternalTradeInputState +import exchange.dydx.abacus.state.internalstate.safeCreate +import exchange.dydx.abacus.state.model.TradeInputField +import exchange.dydx.abacus.state.model.TradeInputField.bracketsExecution +import exchange.dydx.abacus.state.model.TradeInputField.bracketsGoodUntilDuration +import exchange.dydx.abacus.state.model.TradeInputField.bracketsGoodUntilUnit +import exchange.dydx.abacus.state.model.TradeInputField.bracketsStopLossPercent +import exchange.dydx.abacus.state.model.TradeInputField.bracketsStopLossPrice +import exchange.dydx.abacus.state.model.TradeInputField.bracketsStopLossReduceOnly +import exchange.dydx.abacus.state.model.TradeInputField.bracketsTakeProfitPercent +import exchange.dydx.abacus.state.model.TradeInputField.bracketsTakeProfitPrice +import exchange.dydx.abacus.state.model.TradeInputField.bracketsTakeProfitReduceOnly +import exchange.dydx.abacus.state.model.TradeInputField.execution +import exchange.dydx.abacus.state.model.TradeInputField.goodTilDuration +import exchange.dydx.abacus.state.model.TradeInputField.goodTilUnit +import exchange.dydx.abacus.state.model.TradeInputField.leverage +import exchange.dydx.abacus.state.model.TradeInputField.limitPrice +import exchange.dydx.abacus.state.model.TradeInputField.marginMode +import exchange.dydx.abacus.state.model.TradeInputField.postOnly +import exchange.dydx.abacus.state.model.TradeInputField.reduceOnly +import exchange.dydx.abacus.state.model.TradeInputField.side +import exchange.dydx.abacus.state.model.TradeInputField.size +import exchange.dydx.abacus.state.model.TradeInputField.targetLeverage +import exchange.dydx.abacus.state.model.TradeInputField.timeInForceType +import exchange.dydx.abacus.state.model.TradeInputField.trailingPercent +import exchange.dydx.abacus.state.model.TradeInputField.triggerPrice +import exchange.dydx.abacus.state.model.TradeInputField.type +import exchange.dydx.abacus.state.model.TradeInputField.usdcSize +import exchange.dydx.abacus.state.model.TradingStateMachine +import exchange.dydx.abacus.utils.safeSet import kollections.iListOf +import kotlin.math.abs + +internal interface TradeInputProcessorProtocol { + fun tradeInMarket( + inputState: InternalInputState, + marketState: InternalMarketState, + accountState: InternalAccountState, + marketId: String, + subaccountNumber: Int, + ): StateChanges +} + +internal class TradeInputResult( + val changes: StateChanges? = null, + val error: ParsingError? = null, +) internal class TradeInputProcessor( private val parser: ParserProtocol, -) { - fun tradeInMarket( + private val calculator: TradeInputCalculator = TradeInputCalculator(parser, TradeCalculation.trade) +): TradeInputProcessorProtocol { + override fun tradeInMarket( inputState: InternalInputState, marketState: InternalMarketState, accountState: InternalAccountState, @@ -32,35 +94,239 @@ internal class TradeInputProcessor( subaccountNumbers = iListOf(subaccountNumber), ) } + } + + if (inputState.trade.marketId != null) { + // existing trade + inputState.trade.marketId = marketId + inputState.trade.size = null + inputState.trade.price = null + inputState.trade.options = InternalTradeInputOptions() } else { - if (inputState.trade.marketId != null) { - // existing trade - inputState.trade.marketId = marketId - inputState.trade.size = null - inputState.trade.price = null - } else { - // new trade - inputState.trade = initialTradeInputState( - marketId = marketId, + // new trade + inputState.trade = initialTradeInputState( + marketId = marketId, + subaccountNumber = subaccountNumber, + accountState = accountState, + marketState = marketState, + ) + } + + initiateMarginModeLeverage( + trade = inputState.trade, + marketState = marketState, + accountState = accountState, + marketId = marketId, + subaccountNumber = subaccountNumber, + ) + + inputState.currentType = InternalInputType.TRADE + + val subaccountNumbers = + MarginCalculator.getChangedSubaccountNumbers( + parser = parser, + subaccounts = accountState.subaccounts, + subaccountNumber = subaccountNumber, + tradeInput = inputState.trade, + ) + return StateChanges( + changes = iListOf(Changes.subaccount, Changes.input), + markets = null, + subaccountNumbers = subaccountNumbers, + ) + } + + fun trade( + inputState: InternalInputState, + accountState: InternalAccountState, + inputData: String?, + inputType: TradeInputField?, + subaccountNumber: Int, + ): TradeInputResult { + inputState.currentType = InternalInputType.TRADE + + if (inputState.trade.marketId == null) { + // new trade + inputState.trade = initialTradeInputState( + marketId = null, + subaccountNumber = subaccountNumber, + accountState = accountState, + marketState = null, + ) + } + if (inputType == null) { + return TradeInputResult( + changes = StateChanges( + iListOf(Changes.wallet, Changes.subaccount, Changes.input), + null, + iListOf(subaccountNumber), + ) + ) + } + + var error: ParsingError? = null + var changes: StateChanges? = null + + var sizeChanged = false + val trade = inputState.trade + val validInput = inputType.validTradeInputAction?.invoke(trade) ?: true + if (validInput) { + val subaccountNumbers = + MarginCalculator.getChangedSubaccountNumbers( + parser = parser, + subaccounts = accountState.subaccounts, subaccountNumber = subaccountNumber, - accountState = accountState, - marketState = marketState, + tradeInput = trade, ) + when (inputType) { + TradeInputField.type, TradeInputField.side -> { + if (inputData != null) { + if (trade.size?.input == "size.leverage") { + trade.size = TradeInputSize.safeCreate(trade.size).copy(input = "size.size") + } + inputType.updateValueAction?.invoke(trade, inputData, parser) + changes = StateChanges( + changes = iListOf(Changes.subaccount, Changes.input), + markets = null, + subaccountNumbers = subaccountNumbers, + ) + } else { + error = ParsingError( + ParsingErrorType.MissingRequiredData, + "$inputData is not a valid string", + ) + } + } + + TradeInputField.size, + TradeInputField.usdcSize, + TradeInputField.leverage, + TradeInputField.targetLeverage, + -> { + sizeChanged = + (parser.asDouble(inputData) != parser.asDouble(inputType.valueAction?.invoke(trade))) + inputType.updateValueAction?.invoke(trade, inputData, parser) + changes = StateChanges( + changes = iListOf(Changes.subaccount, Changes.input), + markets = null, + subaccountNumbers = subaccountNumbers, + ) + } + + TradeInputField.lastInput, + TradeInputField.limitPrice, + TradeInputField.triggerPrice, + TradeInputField.trailingPercent, + TradeInputField.bracketsStopLossPrice, + TradeInputField.bracketsStopLossPercent, + TradeInputField.bracketsTakeProfitPrice, + TradeInputField.bracketsTakeProfitPercent, + TradeInputField.timeInForceType, + TradeInputField.goodTilUnit, + TradeInputField.bracketsGoodUntilUnit, + TradeInputField.execution, + TradeInputField.bracketsExecution, + TradeInputField.goodTilDuration, + TradeInputField.bracketsGoodUntilDuration, + TradeInputField.reduceOnly, + TradeInputField.postOnly, + TradeInputField.bracketsStopLossReduceOnly, + TradeInputField.bracketsTakeProfitReduceOnly, + -> { + inputType.updateValueAction?.invoke(trade, inputData, parser) + changes = StateChanges( + changes = iListOf(Changes.subaccount, Changes.input), + markets = null, + subaccountNumbers = subaccountNumbers, + ) + } + + TradeInputField.marginMode + -> { + inputType.updateValueAction?.invoke(trade, inputData, parser) + val changedSubaccountNumbers = + MarginCalculator.getChangedSubaccountNumbers( + parser = parser, + subaccounts = accountState.subaccounts, + subaccountNumber = subaccountNumber, + tradeInput = trade + ) + changes = StateChanges( + changes = iListOf(Changes.input, Changes.subaccount), + markets = null, + subaccountNumbers = changedSubaccountNumbers, + ) + } } + + } else { + error = ParsingError.cannotModify(inputType.rawValue) } - return StateChanges( - changes = iListOf(Changes.input), - markets = null, - subaccountNumbers = iListOf(subaccountNumber), + if (sizeChanged) { + when (type) { + TradeInputField.size, + TradeInputField.usdcSize, + TradeInputField.leverage, + -> { + TradeInputSize.safeCreate(trade.size).copy(input = type.rawValue) + } + else -> {} + } + } + + return TradeInputResult( + changes = changes, + error = error, + ) + } + + private fun initiateMarginModeLeverage( + trade: InternalTradeInputState, + marketState: InternalMarketState, + accountState: InternalAccountState, + marketId: String, + subaccountNumber: Int, + ) { + val subaccount = accountState.subaccounts[subaccountNumber] + val existingPosition = MarginCalculator.findExistingPosition( + account = accountState, + marketId = marketId, + subaccountNumber = subaccountNumber, + ) + val existingOrder = MarginCalculator.findExistingOrder( + account = accountState, + marketId = marketId, + subaccountNumber = subaccountNumber, ) + if (existingPosition != null) { + trade.marginMode = + if (subaccount?.equity != null) MarginMode.Isolated else MarginMode.Cross + val currentPositionLeverage = + existingPosition.calculated[CalculationPeriod.current]?.leverage?.abs() + val positionLeverage = + if (currentPositionLeverage != null && currentPositionLeverage > 0) currentPositionLeverage else 1.0 + trade.targetLeverage = positionLeverage + } else if (existingOrder != null) { + trade.marginMode = + if (existingOrder.subaccountNumber == subaccountNumber) MarginMode.Cross else MarginMode.Isolated + trade.targetLeverage = 1.0 + } else { + val marketType = marketState.perpetualMarket?.configs?.perpetualMarketType + trade.marginMode = when (marketType) { + PerpetualMarketType.CROSS -> MarginMode.Cross + PerpetualMarketType.ISOLATED -> MarginMode.Isolated + else -> null + } + trade.targetLeverage = 1.0 + } } private fun initialTradeInputState( marketId: String?, subaccountNumber: Int, accountState: InternalAccountState, - marketState: InternalMarketState, + marketState: InternalMarketState?, ): InternalTradeInputState { // // val trade = exchange.dydx.abacus.utils.mutableMapOf() @@ -91,16 +357,20 @@ internal class TradeInputProcessor( marketId = marketId, subaccountNumber = subaccountNumber, ) ?: MarginCalculator.findMarketMarginMode( - market = marketState.perpetualMarket, + market = marketState?.perpetualMarket, ) + // TODO - implement TradeInputCalculatorV2 + //calculator.calculate() + return InternalTradeInputState( marketId = marketId, size = null, price = null, - type = "LIMIT", - side = "BUY", - marginMode = marginMode, // TODO + type = OrderType.Limit, + side = OrderSide.Buy, + marginMode = marginMode, ) } } + diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/responses/ParsingError.kt b/src/commonMain/kotlin/exchange.dydx.abacus/responses/ParsingError.kt index 7c2b2cb45..7f3a445e7 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/responses/ParsingError.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/responses/ParsingError.kt @@ -46,3 +46,10 @@ class ParsingException( return ParsingError(type = type, message = message ?: "null", stackTrace = stackTrace) } } + +internal fun ParsingError.Companion.cannotModify(typeText: String): ParsingError { + return ParsingError( + ParsingErrorType.InvalidInput, + "$typeText cannot be modified for the selected trade input", + ) +} \ No newline at end of file diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt index 1a744e6ce..f787de50e 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt @@ -21,6 +21,16 @@ import exchange.dydx.abacus.output.account.SubaccountPositionResources import exchange.dydx.abacus.output.account.SubaccountTransfer import exchange.dydx.abacus.output.account.UnbondingDelegation import exchange.dydx.abacus.output.input.MarginMode +import exchange.dydx.abacus.output.input.OrderSide +import exchange.dydx.abacus.output.input.OrderType +import exchange.dydx.abacus.output.input.SelectionOption +import exchange.dydx.abacus.output.input.TradeInputBracket +import exchange.dydx.abacus.output.input.TradeInputBracketSide +import exchange.dydx.abacus.output.input.TradeInputGoodUntil +import exchange.dydx.abacus.output.input.TradeInputMarketOrder +import exchange.dydx.abacus.output.input.TradeInputPrice +import exchange.dydx.abacus.output.input.TradeInputSize +import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.manager.HistoricalTradingRewardsPeriod import exchange.dydx.abacus.utils.NUM_PARENT_SUBACCOUNTS import indexer.codegen.IndexerHistoricalBlockTradingReward @@ -52,12 +62,39 @@ internal data class InternalInputState( internal data class InternalTradeInputState( var marketId: String? = null, - var size: Double? = null, - var price: Double? = null, - var type: String? = null, // TODO: enum - var side: String? = null, // TODO: enum - var marginMode: String? = null, // TODO: enum + var size: TradeInputSize? = null, + var price: TradeInputPrice? = null, + var type: OrderType? = null, + var side: OrderSide? = null, + var marginMode: MarginMode? = null, + var targetLeverage: Double? = null, + var timeInForce: String? = null, + var goodTil: TradeInputGoodUntil? = null, + var execution: String? = null, + var reduceOnly: Boolean = false, + var postOnly: Boolean = false, + var fee: Double? = null, + + var bracket: TradeInputBracket? = null, + var options: InternalTradeInputOptions = InternalTradeInputOptions(), +) +internal data class InternalTradeInputOptions( + var needsMarginMode: Boolean = false, + var needsSize: Boolean = false, + var needsLeverage: Boolean = false, + var maxLeverage: Double? = null, + var needsLimitPrice: Boolean = false, + var needsTargetLeverage: Boolean = false, + var needsTriggerPrice: Boolean = false, + var needsTrailingPercent: Boolean = false, + var needsGoodUntil: Boolean = false, + var needsReduceOnly: Boolean = false, + var needsPostOnly: Boolean = false, + var needsBrackets: Boolean = false, + var timeInForceOptions: SelectionOption? = null, + var executionOptions: SelectionOption? = null, + var marginModeOptions: SelectionOption? = null, ) internal data class InternalMarketSummaryState( @@ -295,3 +332,41 @@ internal data class InternalRewardsParamsState( internal data class InternalLaunchIncentiveState( var seasons: List? = null, ) + +internal fun TradeInputSize.Companion.safeCreate(existing: TradeInputSize?): TradeInputSize { + return existing ?: TradeInputSize( + size = null, + usdcSize = null, + leverage = null, + input = null, + ) +} + +internal fun TradeInputBracket.Companion.safeCreate(existing: TradeInputBracket?): TradeInputBracket { + return existing ?: TradeInputBracket( + stopLoss = null, + takeProfit = null, + goodTil = null, + execution = null, + ) +} + +internal fun TradeInputPrice.Companion.safeCreate(existing: TradeInputPrice?): TradeInputPrice { + return existing ?: TradeInputPrice( + limitPrice = null, + triggerPrice = null, + trailingPercent = null, + ) +} + +internal fun TradeInputBracketSide.Companion.safeCreate(existing: TradeInputBracketSide?): TradeInputBracketSide { + return existing ?: TradeInputBracketSide( + triggerPrice = null, percent = null, reduceOnly = false + ) +} + +internal fun TradeInputGoodUntil.Companion.safeCreate(existing: TradeInputGoodUntil?): TradeInputGoodUntil { + return existing ?: TradeInputGoodUntil( + duration = null, unit = null + ) +} \ No newline at end of file diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+AdjustIsolatedMarginInput.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+AdjustIsolatedMarginInput.kt index 343d70b5b..ec8c0b484 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+AdjustIsolatedMarginInput.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+AdjustIsolatedMarginInput.kt @@ -4,6 +4,7 @@ import exchange.dydx.abacus.calculator.AdjustIsolatedMarginInputCalculator import exchange.dydx.abacus.output.input.IsolatedMarginAdjustmentType import exchange.dydx.abacus.responses.ParsingError import exchange.dydx.abacus.responses.StateResponse +import exchange.dydx.abacus.responses.cannotModify import exchange.dydx.abacus.state.changes.Changes import exchange.dydx.abacus.state.changes.StateChanges import exchange.dydx.abacus.utils.IList @@ -127,7 +128,7 @@ fun TradingStateMachine.adjustIsolatedMargin( } } } else { - error = cannotModify(type.name) + error = ParsingError.cannotModify(type.name) } } else { changes = StateChanges( diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+ClosePositionInput.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+ClosePositionInput.kt index 41c1ec140..26d1e61db 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+ClosePositionInput.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+ClosePositionInput.kt @@ -2,8 +2,11 @@ package exchange.dydx.abacus.state.model import abs import exchange.dydx.abacus.calculator.MarginCalculator +import exchange.dydx.abacus.calculator.TradeCalculation +import exchange.dydx.abacus.calculator.TradeInputCalculator import exchange.dydx.abacus.responses.ParsingError import exchange.dydx.abacus.responses.StateResponse +import exchange.dydx.abacus.responses.cannotModify import exchange.dydx.abacus.state.changes.Changes import exchange.dydx.abacus.state.changes.StateChanges import exchange.dydx.abacus.utils.Numeric @@ -86,7 +89,7 @@ fun TradingStateMachine.closePosition( subaccountNumberChanges, ) } else { - error = cannotModify(typeText) + error = ParsingError.cannotModify(typeText) } } ClosePositionInputField.size.rawValue, ClosePositionInputField.percent.rawValue -> { @@ -145,3 +148,26 @@ fun TradingStateMachine.getPosition( null } } + +private fun TradingStateMachine.initiateClosePosition( + marketId: String?, + subaccountNumber: Int, +): MutableMap { + val trade = mutableMapOf() + trade["type"] = "MARKET" + trade["side"] = "BUY" + trade["marketId"] = marketId ?: "ETH-USD" + + val calculator = TradeInputCalculator(parser, TradeCalculation.closePosition) + val params = mutableMapOf() + params.safeSet("markets", parser.asMap(marketsSummary?.get("markets"))) + params.safeSet("account", account) + params.safeSet("user", user) + params.safeSet("trade", trade) + params.safeSet("rewardsParams", rewardsParams) + params.safeSet("configs", configs) + + val modified = calculator.calculate(params, subaccountNumber, null) + + return parser.asMap(modified["trade"])?.mutable() ?: trade +} \ No newline at end of file diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Errors.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Errors.kt deleted file mode 100644 index bb13db25a..000000000 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Errors.kt +++ /dev/null @@ -1,11 +0,0 @@ -package exchange.dydx.abacus.state.model - -import exchange.dydx.abacus.responses.ParsingError -import exchange.dydx.abacus.responses.ParsingErrorType - -internal fun TradingStateMachine.cannotModify(typeText: String): ParsingError { - return ParsingError( - ParsingErrorType.InvalidInput, - "$typeText cannot be modified for the selected trade input", - ) -} diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Markets.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Markets.kt index 839ca8dba..589021738 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Markets.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+Markets.kt @@ -24,7 +24,7 @@ internal fun TradingStateMachine.receivedMarkets( // TODO remove deprecated marketsSummary = marketsProcessor.subscribedDeprecated(marketsSummary, payload) marketsSummary = marketsCalculator.calculate(parser.asMap(marketsSummary), assets, null) - val subaccountNumbers = MarginCalculator.getChangedSubaccountNumbers( + val subaccountNumbers = MarginCalculator.getChangedSubaccountNumbersDeprecated( parser = parser, account = account, subaccountNumber = subaccountNumber, @@ -54,7 +54,7 @@ internal fun TradingStateMachine.receivedMarketsChanges( } marketsSummary = marketsProcessor.channel_dataDeprecated(marketsSummary, payload) marketsSummary = marketsCalculator.calculate(marketsSummary, assets, payload.keys) - val subaccountNumbers = MarginCalculator.getChangedSubaccountNumbers( + val subaccountNumbers = MarginCalculator.getChangedSubaccountNumbersDeprecated( parser, account, subaccountNumber, @@ -103,7 +103,7 @@ internal fun TradingStateMachine.receivedBatchedMarketsChanges( } } marketsSummary = marketsCalculator.calculate(marketsSummary, assets, keys) - val subaccountNumbers = MarginCalculator.getChangedSubaccountNumbers( + val subaccountNumbers = MarginCalculator.getChangedSubaccountNumbersDeprecated( parser = parser, account = account, subaccountNumber = subaccountNumber, @@ -149,7 +149,7 @@ internal fun TradingStateMachine.processMarketsConfigurations( assets = internalState.assets, keys = null, ) - val subaccountNumbers = MarginCalculator.getChangedSubaccountNumbers( + val subaccountNumbers = MarginCalculator.getChangedSubaccountNumbersDeprecated( parser = parser, account = account, subaccountNumber = subaccountNumber ?: 0, @@ -190,7 +190,7 @@ internal fun TradingStateMachine.receivedMarketsConfigurationsDeprecated( assets = assets, keys = null, ) - val subaccountNumbers = MarginCalculator.getChangedSubaccountNumbers( + val subaccountNumbers = MarginCalculator.getChangedSubaccountNumbersDeprecated( parser = parser, account = account, subaccountNumber = subaccountNumber ?: 0, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt index 6b865e5c1..a455107bc 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt @@ -5,11 +5,15 @@ import exchange.dydx.abacus.calculator.MarginCalculator import exchange.dydx.abacus.calculator.TradeCalculation import exchange.dydx.abacus.calculator.TradeInputCalculator import exchange.dydx.abacus.output.input.MarginMode +import exchange.dydx.abacus.processor.input.TradeInputProcessor import exchange.dydx.abacus.responses.ParsingError import exchange.dydx.abacus.responses.ParsingErrorType import exchange.dydx.abacus.responses.StateResponse +import exchange.dydx.abacus.responses.cannotModify import exchange.dydx.abacus.state.changes.Changes import exchange.dydx.abacus.state.changes.StateChanges +import exchange.dydx.abacus.state.internalstate.InternalMarketState +import exchange.dydx.abacus.state.internalstate.InternalTradeInputState import exchange.dydx.abacus.utils.mutable import exchange.dydx.abacus.utils.mutableMapOf import exchange.dydx.abacus.utils.safeSet @@ -68,7 +72,15 @@ enum class TradeInputField(val rawValue: String) { goodTilDuration, goodTilUnit -> "options.needsGoodUntil" reduceOnly -> "options.needsReduceOnly" postOnly -> "options.needsPostOnly" - bracketsStopLossPrice, bracketsStopLossPercent, bracketsTakeProfitPrice, bracketsTakeProfitPercent, bracketsGoodUntilDuration, bracketsGoodUntilUnit, bracketsStopLossReduceOnly, bracketsTakeProfitReduceOnly, bracketsExecution -> "options.needsBrackets" + bracketsStopLossPrice, + bracketsStopLossPercent, + bracketsTakeProfitPrice, + bracketsTakeProfitPercent, + bracketsGoodUntilDuration, + bracketsGoodUntilUnit, + bracketsStopLossReduceOnly, + bracketsTakeProfitReduceOnly, + bracketsExecution -> "options.needsBrackets" timeInForceType -> "options.timeInForceOptions" execution -> "options.executionOptions" marginMode -> "options.marginModeOptions" @@ -80,6 +92,20 @@ internal fun TradingStateMachine.tradeInMarket( marketId: String, subaccountNumber: Int, ): StateResponse { + if (staticTyping) { + val market = internalState.marketsSummary.markets[marketId] + ?: return StateResponse(state, StateChanges(iListOf()), null) + val changes = tradeInputProcessor.tradeInMarket( + inputState = internalState.input, + marketState = market, + accountState = internalState.wallet.account, + marketId = marketId, + subaccountNumber = subaccountNumber, + ) + updateStateChanges(changes) + return StateResponse(state, changes, null) + } + val input = this.input?.mutable() ?: mutableMapOf() if (parser.asString(parser.value(input, "trade.marketId")) == marketId) { if (parser.asString(parser.value(input, "current")) == "trade") { @@ -147,7 +173,7 @@ internal fun TradingStateMachine.tradeInMarket( input["current"] = "trade" this.input = input val subaccountNumbers = - MarginCalculator.getChangedSubaccountNumbers( + MarginCalculator.getChangedSubaccountNumbersDeprecated( parser, account, subaccountNumber, @@ -195,34 +221,25 @@ private fun TradingStateMachine.initiateTrade( return parser.asMap(modified["trade"])?.mutable() ?: trade } -internal fun TradingStateMachine.initiateClosePosition( - marketId: String?, - subaccountNumber: Int, -): MutableMap { - val trade = mutableMapOf() - trade["type"] = "MARKET" - trade["side"] = "BUY" - trade["marketId"] = marketId ?: "ETH-USD" - - val calculator = TradeInputCalculator(parser, TradeCalculation.closePosition) - val params = mutableMapOf() - params.safeSet("markets", parser.asMap(marketsSummary?.get("markets"))) - params.safeSet("account", account) - params.safeSet("user", user) - params.safeSet("trade", trade) - params.safeSet("rewardsParams", rewardsParams) - params.safeSet("configs", configs) - - val modified = calculator.calculate(params, subaccountNumber, null) - - return parser.asMap(modified["trade"])?.mutable() ?: trade -} - fun TradingStateMachine.trade( data: String?, type: TradeInputField?, subaccountNumber: Int, ): StateResponse { + if (staticTyping) { + val result = tradeInputProcessor.trade( + inputState = internalState.input, + accountState = internalState.wallet.account, + inputType = type, + inputData = data, + subaccountNumber = subaccountNumber, + ) + result.changes?.let { + updateStateChanges(it) + } + return StateResponse(state, result.changes, if (result.error != null) iListOf(result.error) else null) + } + var changes: StateChanges? = null var error: ParsingError? = null val typeText = type?.rawValue @@ -236,7 +253,7 @@ fun TradingStateMachine.trade( if (typeText != null) { if (validTradeInput(trade, typeText)) { val subaccountNumbers = - MarginCalculator.getChangedSubaccountNumbers( + MarginCalculator.getChangedSubaccountNumbersDeprecated( parser, account, subaccountNumber, @@ -308,7 +325,7 @@ fun TradingStateMachine.trade( -> { trade.safeSet(typeText, parser.asString(data)) val changedSubaccountNumbers = - MarginCalculator.getChangedSubaccountNumbers( + MarginCalculator.getChangedSubaccountNumbersDeprecated( parser, account, subaccountNumber, @@ -362,7 +379,7 @@ fun TradingStateMachine.trade( else -> {} } } else { - error = cannotModify(typeText) + error = ParsingError.cannotModify(typeText) } } else { changes = StateChanges( diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TransferInput.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TransferInput.kt index 5ada0caa3..4d3d4c154 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TransferInput.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TransferInput.kt @@ -3,6 +3,7 @@ package exchange.dydx.abacus.state.model import exchange.dydx.abacus.calculator.TransferInputCalculator import exchange.dydx.abacus.responses.ParsingError import exchange.dydx.abacus.responses.StateResponse +import exchange.dydx.abacus.responses.cannotModify import exchange.dydx.abacus.state.changes.Changes import exchange.dydx.abacus.state.changes.StateChanges import exchange.dydx.abacus.utils.mutable @@ -178,7 +179,7 @@ fun TradingStateMachine.transfer( else -> {} } } else { - error = cannotModify(typeText) + error = ParsingError.cannotModify(typeText) } } else { changes = StateChanges(iListOf(Changes.wallet, Changes.subaccount, Changes.input), null, iListOf(subaccountNumber)) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TriggerOrdersInput.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TriggerOrdersInput.kt index 69ced24a8..3eac9a0c9 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TriggerOrdersInput.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TriggerOrdersInput.kt @@ -3,6 +3,7 @@ package exchange.dydx.abacus.state.model import exchange.dydx.abacus.calculator.TriggerOrdersInputCalculator import exchange.dydx.abacus.responses.ParsingError import exchange.dydx.abacus.responses.StateResponse +import exchange.dydx.abacus.responses.cannotModify import exchange.dydx.abacus.state.changes.Changes import exchange.dydx.abacus.state.changes.StateChanges import exchange.dydx.abacus.utils.mutable @@ -133,7 +134,7 @@ fun TradingStateMachine.triggerOrders( else -> {} } } else { - error = cannotModify(typeText) + error = ParsingError.cannotModify(typeText) } } else { changes = 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 47ca5ddaf..dbf6be5ea 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 @@ -43,7 +43,7 @@ internal fun TradingStateMachine.receivedSubaccountSubscribed( changes.add(Changes.historicalPnl) changes.add(Changes.tradingRewards) val subaccountNumber = parser.asInt(payload["subaccountNumber"]) ?: 0 - val subaccountNumbers = MarginCalculator.getChangedSubaccountNumbers( + val subaccountNumbers = MarginCalculator.getChangedSubaccountNumbersDeprecated( parser, account, subaccountNumber ?: 0, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt index 9f9203545..349e7236b 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt @@ -34,6 +34,7 @@ import exchange.dydx.abacus.output.input.ReceiptLine import exchange.dydx.abacus.processor.assets.AssetsProcessor import exchange.dydx.abacus.processor.configs.ConfigsProcessor import exchange.dydx.abacus.processor.configs.RewardsParamsProcessor +import exchange.dydx.abacus.processor.input.TradeInputProcessor import exchange.dydx.abacus.processor.launchIncentive.LaunchIncentiveProcessor import exchange.dydx.abacus.processor.markets.MarketsSummaryProcessor import exchange.dydx.abacus.processor.router.IRouterProcessor @@ -122,6 +123,7 @@ open class TradingStateMachine( } internal val rewardsProcessor = RewardsParamsProcessor(parser) internal val launchIncentiveProcessor = LaunchIncentiveProcessor(parser) + internal val tradeInputProcessor = TradeInputProcessor(parser) internal val marketsCalculator = MarketCalculator(parser) internal val accountCalculator = AccountCalculator(parser, useParentSubaccount) diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/payload/IsolatedMarginModeTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/payload/IsolatedMarginModeTests.kt index 1e2b74cd3..09da93775 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/payload/IsolatedMarginModeTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/payload/IsolatedMarginModeTests.kt @@ -744,7 +744,7 @@ class IsolatedMarginModeTests : V4BaseTests(true) { "marketId" to "ARB-USD", ) - val childSubaccountNumber = MarginCalculator.getChildSubaccountNumberForIsolatedMarginTrade(parser, account, 0, tradeInput) + val childSubaccountNumber = MarginCalculator.getChildSubaccountNumberForIsolatedMarginTradeDeprecated(parser, account, 0, tradeInput) assertEquals(childSubaccountNumber, 256) } From a3916b5cd88af88412f6ee37fe028e03d3373668 Mon Sep 17 00:00:00 2001 From: Rui Date: Mon, 12 Aug 2024 09:36:37 -0700 Subject: [PATCH 25/63] WIP --- .../calculator/MarginCalculator.kt | 7 +- .../calculator/TradeInputCalculator.kt | 2 +- .../calculator/V2/TradeInputCalculatorV2.kt | 294 ++++++++++++++++++ .../V2/TradeInputMarginModeCalculator.kt | 54 ++++ .../output/input/Input.kt | 8 +- .../output/input/ReceiptLine.kt | 46 +++ .../output/input/TradeInput.kt | 198 ++++++++---- .../input/TradeInputField+Actions.kt | 12 +- .../processor/input/TradeInputProcessor.kt | 17 +- .../responses/ParsingError.kt | 2 +- .../state/internalstate/InternalState.kt | 28 +- .../TradingStateMachine+ClosePositionInput.kt | 6 +- .../model/TradingStateMachine+TradeInput.kt | 3 - .../state/model/TradingStateMachine.kt | 9 +- 14 files changed, 576 insertions(+), 110 deletions(-) create mode 100644 src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInputCalculatorV2.kt create mode 100644 src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInputMarginModeCalculator.kt create mode 100644 src/commonMain/kotlin/exchange.dydx.abacus/output/input/ReceiptLine.kt diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginCalculator.kt index 1db726d9c..c0e4032f6 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginCalculator.kt @@ -213,7 +213,8 @@ internal object MarginCalculator { // FE only supports subaccounts that are related to the "main" account (i.e. subaccount 0) and its children // If there are other utilized subaccounts (e.g. subaccount 1 or 129), ignore them as candidates val relevantSubaccounts = subaccounts.filterKeys { - key -> parser.asInt(key)?.let { it % NUM_PARENT_SUBACCOUNTS == 0 } ?: false + key -> + parser.asInt(key)?.let { it % NUM_PARENT_SUBACCOUNTS == 0 } ?: false } val utilizedSubaccountsMarketIdMap = relevantSubaccounts.mapValues { val subaccount = it.value @@ -249,7 +250,7 @@ internal object MarginCalculator { } else if (marketIds.isEmpty()) { // Check if subaccount equity is 0 so that funds are moved to a clean account if reclaimUnutilizedChildSubaccountFunds has not been called yet val equity = subaccounts[subaccountNumberToCheck]?.calculated?.get(CalculationPeriod.current)?.equity ?: 0.0 - if (availableSubaccountNumber == subaccountNumber && equity == 0.0) { + if (availableSubaccountNumber == subaccountNumber && equity == 0.0) { availableSubaccountNumber = subaccountNumberToCheck } } @@ -371,7 +372,7 @@ internal object MarginCalculator { parser = parser, subaccounts = subaccounts, subaccountNumber = subaccountNumber, - marketId = marketId + marketId = marketId, ) if (subaccountNumber != childSubaccountNumber) { return iListOf(subaccountNumber, childSubaccountNumber) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt index 57a9e145a..8387456c7 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt @@ -32,7 +32,7 @@ enum class TradeCalculation(val rawValue: String) { companion object { operator fun invoke(rawValue: String) = - TradeCalculation.values().firstOrNull { it.rawValue == rawValue } + entries.firstOrNull { it.rawValue == rawValue } } } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInputCalculatorV2.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInputCalculatorV2.kt new file mode 100644 index 000000000..9e998a65c --- /dev/null +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInputCalculatorV2.kt @@ -0,0 +1,294 @@ +package exchange.dydx.abacus.calculator.v2 + +import abs +import exchange.dydx.abacus.calculator.CalculationPeriod +import exchange.dydx.abacus.calculator.TradeCalculation +import exchange.dydx.abacus.output.input.OrderSide +import exchange.dydx.abacus.output.input.OrderType +import exchange.dydx.abacus.output.input.TradeInputSize +import exchange.dydx.abacus.state.internalstate.InternalMarketState +import exchange.dydx.abacus.state.internalstate.InternalMarketSummaryState +import exchange.dydx.abacus.state.internalstate.InternalOrderbookTick +import exchange.dydx.abacus.state.internalstate.InternalRewardsParamsState +import exchange.dydx.abacus.state.internalstate.InternalSubaccountState +import exchange.dydx.abacus.state.internalstate.InternalTradeInputState +import exchange.dydx.abacus.state.internalstate.InternalUserState +import exchange.dydx.abacus.state.internalstate.InternalWalletState +import exchange.dydx.abacus.state.internalstate.safeCreate +import exchange.dydx.abacus.state.model.ClosePositionInputField +import exchange.dydx.abacus.utils.Numeric +import exchange.dydx.abacus.utils.Rounder +import exchange.dydx.abacus.utils.mutable +import exchange.dydx.abacus.utils.safeSet + +internal class TradeInputCalculatorV2( + private val calculation: TradeCalculation, + private val marginModeCalculator: TradeInputMarginModeCalculator = TradeInputMarginModeCalculator(), +) { + fun calculate( + trade: InternalTradeInputState, + wallet: InternalWalletState, + marketSummary: InternalMarketSummaryState, + rewardsParams: InternalRewardsParamsState?, + subaccountNumber: Int, + input: String?, + ): InternalTradeInputState { + val account = wallet.account + val subaccount = + account.groupedSubaccounts[subaccountNumber] ?: account.subaccounts[subaccountNumber] + val user = wallet.user + val markets = marketSummary.markets + + marginModeCalculator.updateTradeInputMarginMode( + tradeInput = trade, + markets = markets, + account = account, + subaccountNumber = subaccountNumber, + ) + + if (input != null) { + when (trade.type) { + OrderType.Market, + OrderType.StopMarket, + OrderType.TakeProfitMarket -> + calculateMarketOrderTrade( + trade = trade, + market = markets[trade.marketId], + subaccount = subaccount, + user = user, + input = input, + ) + + OrderType.Limit -> TODO() + OrderType.StopLimit -> TODO() + OrderType.TakeProfitLimit -> TODO() + OrderType.TrailingStop -> TODO() + OrderType.Liquidated -> TODO() + OrderType.Liquidation -> TODO() + OrderType.Offsetting -> TODO() + OrderType.Deleveraged -> TODO() + OrderType.FinalSettlement -> TODO() + null -> TODO() + } + } + + return trade + } + + private fun calculateMarketOrderTrade( + trade: InternalTradeInputState, + market: InternalMarketState?, + subaccount: InternalSubaccountState?, + user: InternalUserState?, + input: String?, + ): InternalTradeInputState { + if (calculation == TradeCalculation.closePosition) { + calculateClosePositionSize(trade, market, subaccount) + } + val marketOrder = calculateMarketOrder(modified, market, subaccount, user, isBuying, input) + val filled = parser.asBool(marketOrder?.get("filled")) ?: false + val tradeSize = parser.asNativeMap(modified["size"])?.mutable() + when (input) { + "size.size", "size.percent" -> tradeSize?.safeSet( + "usdcSize", + if (filled) parser.asDouble(marketOrder?.get("usdcSize")) else null, + ) + + "size.usdcSize" -> tradeSize?.safeSet( + "size", + if (filled) parser.asDouble(marketOrder?.get("size")) else null, + ) + + "size.leverage" -> { + tradeSize?.safeSet( + "size", + if (filled) parser.asDouble(marketOrder?.get("size")) else null, + ) + tradeSize?.safeSet( + "usdcSize", + if (filled) parser.asDouble(marketOrder?.get("usdcSize")) else null, + ) + + val orderbook = parser.asNativeMap(market?.get("orderbook_consolidated")) + if (marketOrder != null && orderbook != null) { + val side = side(marketOrder, orderbook) + if (side != null && side != parser.asString(modified["side"])) { + modified.safeSet("side", side) + } + } + } + } + modified.safeSet("marketOrder", marketOrder) + modified.safeSet("size", tradeSize) + + return modified + } + + private fun calculateClosePositionSize( + trade: InternalTradeInputState, + market: InternalMarketState?, + subaccount: InternalSubaccountState?, + ): InternalTradeInputState { + val inputType = ClosePositionInputField.invoke(trade.size?.input) + val marketId = trade.marketId ?: return trade + val position = subaccount?.openPositions?.get(marketId) ?: return trade + val positionSize = position.calculated[CalculationPeriod.current]?.size ?: return trade + val positionSizeAbs = positionSize.abs() + trade.side = if (positionSize > Numeric.double.ZERO) OrderSide.Sell else OrderSide.Buy + when (inputType) { + ClosePositionInputField.percent -> { + val percent = trade.sizePercent ?: return trade + val size = + if (percent > Numeric.double.ONE) positionSizeAbs else positionSizeAbs * percent + val stepSize = market?.perpetualMarket?.configs?.stepSize ?: return trade + trade.size = + TradeInputSize.safeCreate(trade.size).copy(size = Rounder.round(size, stepSize)) + return trade + } + + ClosePositionInputField.size -> { + trade.sizePercent = null + val size = trade.size?.size ?: return trade + if (size > positionSizeAbs) { + trade.size = TradeInputSize.safeCreate(trade.size).copy(size = positionSizeAbs) + } + } + + else -> {} + } + return trade + } + + private fun calculateMarketOrder( + trade: InternalTradeInputState, + market: InternalMarketState?, + subaccount: InternalSubaccountState?, + user: InternalUserState?, + input: String?, + ): Map? { + val tradeSize = trade.size + if (tradeSize != null) { + return when (input) { + "size.size", "size.percent" -> { + val orderbook = getOrderbook(market = market, isBuying = trade.isBuying) + calculateMarketOrderFromSize( + parser.asDouble(tradeSize["size"]), + orderbook, + ) + } + + "size.usdcSize" -> { + val stepSize = + parser.asDouble(parser.value(market, "configs.stepSize")) + ?: 0.001 + val orderbook = orderbook(market, isBuying) + calculateMarketOrderFromUsdcSize( + parser.asDouble(tradeSize["usdcSize"]), + orderbook, + stepSize, + ) + } + + "size.leverage" -> { + val leverage = + parser.asDouble(parser.value(trade, "size.leverage")) ?: return null + calculateMarketOrderFromLeverage( + leverage, + market, + subaccount, + user, + ) + } + + else -> null + } + } + return null + } + + private fun getOrderbook( + market: InternalMarketState?, + isBuying: Boolean?, + ): List? { + return when (isBuying) { + true -> return market?.consolidatedOrderbook?.asks + false -> return market?.consolidatedOrderbook?.bids + else -> return null + } + } + + private fun calculateMarketOrderFromSize( + size: Double?, + orderbook: List?, + ): Map? { + return if (size != null && size != Numeric.double.ZERO) { + if (orderbook != null) { + val desiredSize = size + var sizeTotal = Numeric.double.ZERO + var usdcSizeTotal = Numeric.double.ZERO + var worstPrice: Double? = null + var filled = false + val marketOrderOrderBook = mutableListOf() + orderbookLoop@ for (element in orderbook) { + val entry = element + val entryPrice = entry.price + val entrySize = entry.size + + filled = (sizeTotal + entrySize >= size) + + val matchedSize = if (filled) (desiredSize - sizeTotal) else entrySize + val matchedUsdcSize = matchedSize * entryPrice + + sizeTotal += matchedSize + usdcSizeTotal += matchedUsdcSize + + worstPrice = entryPrice + marketOrderOrderBook.add(entry.copy(size = matchedSize)) + if (filled) { + break@orderbookLoop + } + } + marketOrder( + marketOrderOrderBook, + sizeTotal, + usdcSizeTotal, + worstPrice, + filled, + ) + } else { + marketOrder( + mutableListOf>(), + parser.asDouble(size)!!, + Numeric.double.ZERO, + null, + false, + ) + } + } else { + null + } + } + + private fun marketOrder( + orderbook: List>, + size: Double?, + usdcSize: Double?, + worstPrice: Double?, + filled: Boolean, + ): Map? { + return if (size != null && usdcSize != null) { + val marketOrder = exchange.dydx.abacus.utils.mutableMapOf() + marketOrder.safeSet("orderbook", orderbook) + if (size != Numeric.double.ZERO) { + marketOrder.safeSet("price", (usdcSize / size)) + } + marketOrder.safeSet("size", size) + marketOrder.safeSet("usdcSize", usdcSize) + marketOrder.safeSet("worstPrice", worstPrice) + marketOrder.safeSet("filled", filled) + marketOrder + } else { + null + } + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInputMarginModeCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInputMarginModeCalculator.kt new file mode 100644 index 000000000..7ef843c7b --- /dev/null +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInputMarginModeCalculator.kt @@ -0,0 +1,54 @@ +package exchange.dydx.abacus.calculator.v2 + +import exchange.dydx.abacus.calculator.CalculationPeriod +import exchange.dydx.abacus.calculator.MarginCalculator +import exchange.dydx.abacus.output.input.MarginMode +import exchange.dydx.abacus.state.internalstate.InternalAccountState +import exchange.dydx.abacus.state.internalstate.InternalMarketState +import exchange.dydx.abacus.state.internalstate.InternalTradeInputState + +internal class TradeInputMarginModeCalculator { + fun updateTradeInputMarginMode( + tradeInput: InternalTradeInputState, + markets: Map?, + account: InternalAccountState, + subaccountNumber: Int, + ): InternalTradeInputState { + val existingMarginMode = MarginCalculator.findExistingMarginMode( + account = account, + marketId = tradeInput.marketId, + subaccountNumber = subaccountNumber, + ) + + // If there is an existing position or order, we have to use the same margin mode + if (existingMarginMode != null) { + tradeInput.marginMode = existingMarginMode + if ( existingMarginMode == MarginMode.Isolated && tradeInput.targetLeverage == null) { + val existingPosition = MarginCalculator.findExistingPosition( + account = account, + marketId = tradeInput.marketId, + subaccountNumber = subaccountNumber, + ) + val existingPositionLeverage = existingPosition?.calculated?.get(CalculationPeriod.current)?.leverage + tradeInput.targetLeverage = existingPositionLeverage ?: 1.0 + } + } else { + val marketMarginMode = MarginCalculator.findMarketMarginMode( + market = markets?.get(tradeInput.marketId)?.perpetualMarket, + ) + when (marketMarginMode) { + MarginMode.Isolated -> { + tradeInput.marginMode = marketMarginMode + tradeInput.targetLeverage = tradeInput.targetLeverage ?: 1.0 + } + + MarginMode.Cross -> { + if (tradeInput.marginMode == null) { + tradeInput.marginMode = marketMarginMode + } + } + } + } + return tradeInput + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/output/input/Input.kt b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/Input.kt index b18c2c9b8..7bd4d7656 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/output/input/Input.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/Input.kt @@ -42,14 +42,18 @@ data class Input( parser: ParserProtocol, data: Map<*, *>?, environment: V4Environment?, - internalState: InternalState? + internalState: InternalState?, + staticTyping: Boolean, ): Input? { Logger.d { "creating Input\n" } data?.let { val current = InputType.invoke(parser.asString(data["current"])) - val trade = + val trade = if (staticTyping) { + TradeInput.create(state = internalState?.input?.trade) + } else { TradeInput.create(existing?.trade, parser, parser.asMap(data["trade"])) + } val closePosition = ClosePositionInput.create(existing?.closePosition, parser, parser.asMap(data["closePosition"])) val transfer = diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/output/input/ReceiptLine.kt b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/ReceiptLine.kt new file mode 100644 index 000000000..d5a6995b2 --- /dev/null +++ b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/ReceiptLine.kt @@ -0,0 +1,46 @@ +package exchange.dydx.abacus.output.input + +import exchange.dydx.abacus.protocols.ParserProtocol +import exchange.dydx.abacus.utils.IList +import kollections.JsExport +import kollections.toIList +import kotlinx.serialization.Serializable + +@JsExport +@Serializable +enum class ReceiptLine(val rawValue: String) { + Equity("EQUITY"), + BuyingPower("BUYING_POWER"), + MarginUsage("MARGIN_USAGE"), + ExpectedPrice("EXPECTED_PRICE"), + Fee("FEE"), + Total("TOTAL"), + WalletBalance("WALLET_BALANCE"), + BridgeFee("BRIDGE_FEE"), + ExchangeRate("EXCHANGE_RATE"), + ExchangeReceived("EXCHANGE_RECEIVED"), + Slippage("SLIPPAGE"), + GasFee("GAS_FEES"), + Reward("REWARD"), + TransferRouteEstimatedDuration("TRANSFER_ROUTE_ESTIMATE_DURATION"), + CrossFreeCollateral("CROSS_FREE_COLLATERAL"), + CrossMarginUsage("CROSS_MARGIN_USAGE"), + PositionMargin("POSITION_MARGIN"), + PositionLeverage("POSITION_LEVERAGE"), + LiquidationPrice("LIQUIDATION_PRICE"); + + companion object { + operator fun invoke(rawValue: String) = + ReceiptLine.values().firstOrNull { it.rawValue == rawValue } + + internal fun create( + parser: ParserProtocol, + data: List?, + ): IList? { + return data?.mapNotNull { + val string = parser.asString(it) + if (string != null) invoke(string) else null + }?.toIList() + } + } +} 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 1c54e8f23..950375bf7 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/output/input/TradeInput.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/TradeInput.kt @@ -1,6 +1,8 @@ package exchange.dydx.abacus.output.input import exchange.dydx.abacus.protocols.ParserProtocol +import exchange.dydx.abacus.state.internalstate.InternalTradeInputOptions +import exchange.dydx.abacus.state.internalstate.InternalTradeInputState import exchange.dydx.abacus.utils.IList import exchange.dydx.abacus.utils.IMutableList import exchange.dydx.abacus.utils.Logger @@ -85,47 +87,118 @@ data class TradeInputOptions( private val typeOptionsV4Array = iListOf( SelectionOption( - OrderType.Limit.rawValue, - null, - "APP.TRADE.LIMIT_ORDER_SHORT", - null, + type = OrderType.Limit.rawValue, + string = null, + stringKey = "APP.TRADE.LIMIT_ORDER_SHORT", + iconUrl = null, ), SelectionOption( - OrderType.Market.rawValue, - null, - "APP.TRADE.MARKET_ORDER_SHORT", - null, + type = OrderType.Market.rawValue, + string = null, + stringKey = "APP.TRADE.MARKET_ORDER_SHORT", + iconUrl = null, ), - SelectionOption(OrderType.StopLimit.rawValue, null, "APP.TRADE.STOP_LIMIT", null), - SelectionOption(OrderType.StopMarket.rawValue, null, "APP.TRADE.STOP_MARKET", null), SelectionOption( - OrderType.TakeProfitLimit.rawValue, - null, - "APP.TRADE.TAKE_PROFIT", - null, + type = OrderType.StopLimit.rawValue, + string = null, + stringKey = "APP.TRADE.STOP_LIMIT", + iconUrl = null, ), SelectionOption( - OrderType.TakeProfitMarket.rawValue, - null, - "APP.TRADE.TAKE_PROFIT_MARKET", - null, + type = OrderType.StopMarket.rawValue, + string = null, + stringKey = "APP.TRADE.STOP_MARKET", + iconUrl = null, + ), + SelectionOption( + type = OrderType.TakeProfitLimit.rawValue, + string = null, + stringKey = "APP.TRADE.TAKE_PROFIT", + iconUrl = null, + ), + SelectionOption( + type = OrderType.TakeProfitMarket.rawValue, + string = null, + stringKey = "APP.TRADE.TAKE_PROFIT_MARKET", + iconUrl = null, ), ) private val sideOptionsArray = iListOf( - SelectionOption(OrderSide.Buy.rawValue, null, "APP.GENERAL.BUY", null), - SelectionOption(OrderSide.Sell.rawValue, null, "APP.GENERAL.SELL", null), + SelectionOption( + type = OrderSide.Buy.rawValue, + string = null, + stringKey = "APP.GENERAL.BUY", + iconUrl = null, + ), + SelectionOption( + type = OrderSide.Sell.rawValue, + string = null, + stringKey = "APP.GENERAL.SELL", + iconUrl = null, + ), ) private val goodTilUnitOptionsArray = iListOf( - SelectionOption("M", null, "APP.GENERAL.TIME_STRINGS.MINUTES_SHORT", null), - SelectionOption("H", null, "APP.GENERAL.TIME_STRINGS.HOURS", null), - SelectionOption("D", null, "APP.GENERAL.TIME_STRINGS.DAYS", null), - SelectionOption("W", null, "APP.GENERAL.TIME_STRINGS.WEEKS", null), + SelectionOption( + type = "M", + string = null, + stringKey = "APP.GENERAL.TIME_STRINGS.MINUTES_SHORT", + iconUrl = null, + ), + SelectionOption( + type = "H", + string = null, + stringKey = "APP.GENERAL.TIME_STRINGS.HOURS", + iconUrl = null, + ), + SelectionOption( + type = "D", + string = null, + stringKey = "APP.GENERAL.TIME_STRINGS.DAYS", + iconUrl = null, + ), + SelectionOption( + type = "W", + string = null, + stringKey = "APP.GENERAL.TIME_STRINGS.WEEKS", + iconUrl = null, + ), ) + internal fun create( + state: InternalTradeInputOptions?, + ): TradeInputOptions? { + if (state == null) { + return null + } + + return TradeInputOptions( + needsMarginMode = state.needsMarginMode, + needsSize = state.needsSize, + needsLeverage = state.needsLeverage, + maxLeverage = state.maxLeverage, + needsLimitPrice = state.needsLimitPrice, + needsTargetLeverage = state.needsTargetLeverage, + needsTriggerPrice = state.needsTriggerPrice, + needsTrailingPercent = state.needsTrailingPercent, + needsGoodUntil = state.needsGoodUntil, + needsReduceOnly = state.needsReduceOnly, + needsPostOnly = state.needsPostOnly, + needsBrackets = state.needsBrackets, + typeOptions = typeOptionsV4Array, + sideOptions = sideOptionsArray, + timeInForceOptions = state.timeInForceOptions?.toIList(), + goodTilUnitOptions = goodTilUnitOptionsArray, + executionOptions = state.executionOptions?.toIList(), + marginModeOptions = state.marginModeOptions?.toIList(), + reduceOnlyTooltip = state.reduceOnlyTooltip, + postOnlyTooltip = state.postOnlyTooltip, + ) + } + internal fun create( existing: TradeInputOptions?, parser: ParserProtocol, @@ -156,9 +229,9 @@ data class TradeInputOptions( for (i in data.indices) { val item = data[i] SelectionOption.create( - existing?.marginModeOptions?.getOrNull(i), - parser, - parser.asMap(item), + existing = existing?.marginModeOptions?.getOrNull(i), + parser = parser, + data = parser.asMap(item), )?.let { marginModeOptions?.add(it) } @@ -250,9 +323,9 @@ data class TradeInputOptions( return null } - fun buildToolTip(stringKey: String?): Tooltip? { + private fun buildToolTip(stringKey: String?): Tooltip? { return if (stringKey != null) { - Tooltip("$stringKey.TITLE", "$stringKey.BODY") + Tooltip(titleStringKey = "$stringKey.TITLE", bodyStringKey = "$stringKey.BODY") } else { null } @@ -700,45 +773,6 @@ enum class OrderTimeInForce(val rawValue: String) { } } -@JsExport -@Serializable -enum class ReceiptLine(val rawValue: String) { - Equity("EQUITY"), - BuyingPower("BUYING_POWER"), - MarginUsage("MARGIN_USAGE"), - ExpectedPrice("EXPECTED_PRICE"), - Fee("FEE"), - Total("TOTAL"), - WalletBalance("WALLET_BALANCE"), - BridgeFee("BRIDGE_FEE"), - ExchangeRate("EXCHANGE_RATE"), - ExchangeReceived("EXCHANGE_RECEIVED"), - Slippage("SLIPPAGE"), - GasFee("GAS_FEES"), - Reward("REWARD"), - TransferRouteEstimatedDuration("TRANSFER_ROUTE_ESTIMATE_DURATION"), - CrossFreeCollateral("CROSS_FREE_COLLATERAL"), - CrossMarginUsage("CROSS_MARGIN_USAGE"), - PositionMargin("POSITION_MARGIN"), - PositionLeverage("POSITION_LEVERAGE"), - LiquidationPrice("LIQUIDATION_PRICE"); - - companion object { - operator fun invoke(rawValue: String) = - ReceiptLine.values().firstOrNull { it.rawValue == rawValue } - - internal fun create( - parser: ParserProtocol, - data: List?, - ): IList? { - return data?.mapNotNull { - val string = parser.asString(it) - if (string != null) invoke(string) else null - }?.toIList() - } - } -} - @JsExport @Serializable data class TradeInput( @@ -761,6 +795,34 @@ data class TradeInput( val summary: TradeInputSummary?, ) { companion object { + internal fun create( + state: InternalTradeInputState? + ): TradeInput? { + if (state == null) { + return null + } + + return TradeInput( + type = state.type, + side = state.side, + marketId = state.marketId, + size = state.size, + price = state.price, + timeInForce = state.timeInForce, + goodTil = state.goodTil, + execution = state.execution, + reduceOnly = state.reduceOnly, + postOnly = state.postOnly, + fee = state.fee, + marginMode = state.marginMode ?: MarginMode.Cross, + targetLeverage = state.targetLeverage ?: 1.0, + bracket = state.bracket, + marketOrder = null, // TODO + options = TradeInputOptions.create(state.options), + summary = null, // TODO + ) + } + internal fun create( existing: TradeInput?, parser: ParserProtocol, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputField+Actions.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputField+Actions.kt index 39a409d00..ca1865125 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputField+Actions.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputField+Actions.kt @@ -46,7 +46,7 @@ import exchange.dydx.abacus.state.model.TradeInputField.usdcSize internal val TradeInputField.validTradeInputAction: ((InternalTradeInputState) -> Boolean)? get() = when (this) { type, side -> null - size, usdcSize, leverage -> { it -> it.options.needsSize } + size, usdcSize, leverage -> { it -> it.options.needsSize } limitPrice -> { it -> it.options.needsLimitPrice } triggerPrice -> { it -> it.options.needsTriggerPrice } trailingPercent -> { it -> it.options.needsTrailingPercent } @@ -171,7 +171,7 @@ internal val TradeInputField.updateValueAction: ((InternalTradeInputState, Strin bracketsGoodUntilUnit -> { trade, value, parser -> val braket = TradeInputBracket.safeCreate(trade.bracket) trade.bracket = braket.copy( - goodTil = TradeInputGoodUntil.safeCreate(braket.goodTil).copy(unit = value) + goodTil = TradeInputGoodUntil.safeCreate(braket.goodTil).copy(unit = value), ) } @@ -190,7 +190,7 @@ internal val TradeInputField.updateValueAction: ((InternalTradeInputState, Strin val braket = TradeInputBracket.safeCreate(trade.bracket) trade.bracket = braket.copy( goodTil = TradeInputGoodUntil.safeCreate(braket.goodTil) - .copy(duration = parser.asDouble(value)) + .copy(duration = parser.asDouble(value)), ) } @@ -210,8 +210,8 @@ internal val TradeInputField.updateValueAction: ((InternalTradeInputState, Strin val takeProfit = TradeInputBracketSide.safeCreate(braket.takeProfit) trade.bracket = braket.copy( takeProfit = takeProfit.copy( - reduceOnly = parser.asBool(value) ?: false - ) + reduceOnly = parser.asBool(value) ?: false, + ), ) } @@ -229,4 +229,4 @@ internal val TradeInputField.updateValueAction: ((InternalTradeInputState, Strin trade.size = TradeInputSize.safeCreate(trade.size).copy(leverage = parser.asDouble(value)) } - } \ No newline at end of file + } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputProcessor.kt index c816398ce..089010650 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputProcessor.kt @@ -9,9 +9,6 @@ import exchange.dydx.abacus.output.PerpetualMarketType import exchange.dydx.abacus.output.input.MarginMode import exchange.dydx.abacus.output.input.OrderSide import exchange.dydx.abacus.output.input.OrderType -import exchange.dydx.abacus.output.input.TradeInputBracket -import exchange.dydx.abacus.output.input.TradeInputBracketSide -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 @@ -52,8 +49,6 @@ import exchange.dydx.abacus.state.model.TradeInputField.trailingPercent import exchange.dydx.abacus.state.model.TradeInputField.triggerPrice import exchange.dydx.abacus.state.model.TradeInputField.type import exchange.dydx.abacus.state.model.TradeInputField.usdcSize -import exchange.dydx.abacus.state.model.TradingStateMachine -import exchange.dydx.abacus.utils.safeSet import kollections.iListOf import kotlin.math.abs @@ -75,7 +70,7 @@ internal class TradeInputResult( internal class TradeInputProcessor( private val parser: ParserProtocol, private val calculator: TradeInputCalculator = TradeInputCalculator(parser, TradeCalculation.trade) -): TradeInputProcessorProtocol { +) : TradeInputProcessorProtocol { override fun tradeInMarket( inputState: InternalInputState, marketState: InternalMarketState, @@ -160,7 +155,7 @@ internal class TradeInputProcessor( iListOf(Changes.wallet, Changes.subaccount, Changes.input), null, iListOf(subaccountNumber), - ) + ), ) } @@ -182,7 +177,7 @@ internal class TradeInputProcessor( TradeInputField.type, TradeInputField.side -> { if (inputData != null) { if (trade.size?.input == "size.leverage") { - trade.size = TradeInputSize.safeCreate(trade.size).copy(input = "size.size") + trade.size = TradeInputSize.safeCreate(trade.size).copy(input = "size.size") } inputType.updateValueAction?.invoke(trade, inputData, parser) changes = StateChanges( @@ -249,7 +244,7 @@ internal class TradeInputProcessor( parser = parser, subaccounts = accountState.subaccounts, subaccountNumber = subaccountNumber, - tradeInput = trade + tradeInput = trade, ) changes = StateChanges( changes = iListOf(Changes.input, Changes.subaccount), @@ -258,7 +253,6 @@ internal class TradeInputProcessor( ) } } - } else { error = ParsingError.cannotModify(inputType.rawValue) } @@ -361,7 +355,7 @@ internal class TradeInputProcessor( ) // TODO - implement TradeInputCalculatorV2 - //calculator.calculate() + // calculator.calculate() return InternalTradeInputState( marketId = marketId, @@ -373,4 +367,3 @@ internal class TradeInputProcessor( ) } } - diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/responses/ParsingError.kt b/src/commonMain/kotlin/exchange.dydx.abacus/responses/ParsingError.kt index 7f3a445e7..16e61b317 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/responses/ParsingError.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/responses/ParsingError.kt @@ -52,4 +52,4 @@ internal fun ParsingError.Companion.cannotModify(typeText: String): ParsingError ParsingErrorType.InvalidInput, "$typeText cannot be modified for the selected trade input", ) -} \ No newline at end of file +} diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt index f787de50e..73a5429dd 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt @@ -24,13 +24,12 @@ import exchange.dydx.abacus.output.input.MarginMode import exchange.dydx.abacus.output.input.OrderSide import exchange.dydx.abacus.output.input.OrderType import exchange.dydx.abacus.output.input.SelectionOption +import exchange.dydx.abacus.output.input.Tooltip import exchange.dydx.abacus.output.input.TradeInputBracket import exchange.dydx.abacus.output.input.TradeInputBracketSide import exchange.dydx.abacus.output.input.TradeInputGoodUntil -import exchange.dydx.abacus.output.input.TradeInputMarketOrder import exchange.dydx.abacus.output.input.TradeInputPrice import exchange.dydx.abacus.output.input.TradeInputSize -import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.manager.HistoricalTradingRewardsPeriod import exchange.dydx.abacus.utils.NUM_PARENT_SUBACCOUNTS import indexer.codegen.IndexerHistoricalBlockTradingReward @@ -63,6 +62,7 @@ internal data class InternalInputState( internal data class InternalTradeInputState( var marketId: String? = null, var size: TradeInputSize? = null, + var sizePercent: Double? = null, var price: TradeInputPrice? = null, var type: OrderType? = null, var side: OrderSide? = null, @@ -77,11 +77,14 @@ internal data class InternalTradeInputState( var bracket: TradeInputBracket? = null, var options: InternalTradeInputOptions = InternalTradeInputOptions(), -) +) { + val isBuying: Boolean + get() = side == OrderSide.Buy || side == null +} internal data class InternalTradeInputOptions( var needsMarginMode: Boolean = false, - var needsSize: Boolean = false, + var needsSize: Boolean = false, var needsLeverage: Boolean = false, var maxLeverage: Double? = null, var needsLimitPrice: Boolean = false, @@ -92,9 +95,11 @@ internal data class InternalTradeInputOptions( var needsReduceOnly: Boolean = false, var needsPostOnly: Boolean = false, var needsBrackets: Boolean = false, - var timeInForceOptions: SelectionOption? = null, - var executionOptions: SelectionOption? = null, - var marginModeOptions: SelectionOption? = null, + var timeInForceOptions: List? = null, + var executionOptions: List? = null, + var marginModeOptions: List? = null, + var reduceOnlyTooltip: Tooltip? = null, + var postOnlyTooltip: Tooltip? = null, ) internal data class InternalMarketSummaryState( @@ -361,12 +366,15 @@ internal fun TradeInputPrice.Companion.safeCreate(existing: TradeInputPrice?): T internal fun TradeInputBracketSide.Companion.safeCreate(existing: TradeInputBracketSide?): TradeInputBracketSide { return existing ?: TradeInputBracketSide( - triggerPrice = null, percent = null, reduceOnly = false + triggerPrice = null, + percent = null, + reduceOnly = false, ) } internal fun TradeInputGoodUntil.Companion.safeCreate(existing: TradeInputGoodUntil?): TradeInputGoodUntil { return existing ?: TradeInputGoodUntil( - duration = null, unit = null + duration = null, + unit = null, ) -} \ No newline at end of file +} diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+ClosePositionInput.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+ClosePositionInput.kt index 26d1e61db..904ebb801 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+ClosePositionInput.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+ClosePositionInput.kt @@ -25,8 +25,8 @@ enum class ClosePositionInputField(val rawValue: String) { percent("size.percent"); companion object { - operator fun invoke(rawValue: String) = - ClosePositionInputField.values().firstOrNull { it.rawValue == rawValue } + operator fun invoke(rawValue: String?) = + entries.firstOrNull { it.rawValue == rawValue } } } @@ -170,4 +170,4 @@ private fun TradingStateMachine.initiateClosePosition( val modified = calculator.calculate(params, subaccountNumber, null) return parser.asMap(modified["trade"])?.mutable() ?: trade -} \ No newline at end of file +} diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt index a455107bc..a1d2f5751 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt @@ -5,15 +5,12 @@ import exchange.dydx.abacus.calculator.MarginCalculator import exchange.dydx.abacus.calculator.TradeCalculation import exchange.dydx.abacus.calculator.TradeInputCalculator import exchange.dydx.abacus.output.input.MarginMode -import exchange.dydx.abacus.processor.input.TradeInputProcessor import exchange.dydx.abacus.responses.ParsingError import exchange.dydx.abacus.responses.ParsingErrorType import exchange.dydx.abacus.responses.StateResponse import exchange.dydx.abacus.responses.cannotModify import exchange.dydx.abacus.state.changes.Changes import exchange.dydx.abacus.state.changes.StateChanges -import exchange.dydx.abacus.state.internalstate.InternalMarketState -import exchange.dydx.abacus.state.internalstate.InternalTradeInputState import exchange.dydx.abacus.utils.mutable import exchange.dydx.abacus.utils.mutableMapOf import exchange.dydx.abacus.utils.safeSet diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt index 349e7236b..f9e6d4f1f 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt @@ -1466,7 +1466,14 @@ open class TradingStateMachine( environment = this.environment, ) this.input?.let { - input = Input.create(input, parser, it, environment, internalState) + input = Input.create( + existing = input, + parser = parser, + data = it, + environment = environment, + internalState = internalState, + staticTyping = staticTyping, + ) } } } From 222d283b7141dc04ab577a068fe120e173c55240 Mon Sep 17 00:00:00 2001 From: Rui Date: Mon, 12 Aug 2024 11:48:57 -0700 Subject: [PATCH 26/63] WIP --- .../calculator/V2/TradeInputCalculatorV2.kt | 396 ++++++++++++++---- .../output/input/TradeInput.kt | 1 - .../state/internalstate/InternalState.kt | 3 +- 3 files changed, 325 insertions(+), 75 deletions(-) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInputCalculatorV2.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInputCalculatorV2.kt index 9e998a65c..479e19e17 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInputCalculatorV2.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInputCalculatorV2.kt @@ -5,6 +5,8 @@ import exchange.dydx.abacus.calculator.CalculationPeriod import exchange.dydx.abacus.calculator.TradeCalculation import exchange.dydx.abacus.output.input.OrderSide import exchange.dydx.abacus.output.input.OrderType +import exchange.dydx.abacus.output.input.OrderbookUsage +import exchange.dydx.abacus.output.input.TradeInputMarketOrder import exchange.dydx.abacus.output.input.TradeInputSize import exchange.dydx.abacus.state.internalstate.InternalMarketState import exchange.dydx.abacus.state.internalstate.InternalMarketSummaryState @@ -20,6 +22,7 @@ import exchange.dydx.abacus.utils.Numeric import exchange.dydx.abacus.utils.Rounder import exchange.dydx.abacus.utils.mutable import exchange.dydx.abacus.utils.safeSet +import kollections.toIList internal class TradeInputCalculatorV2( private val calculation: TradeCalculation, @@ -85,30 +88,28 @@ internal class TradeInputCalculatorV2( if (calculation == TradeCalculation.closePosition) { calculateClosePositionSize(trade, market, subaccount) } - val marketOrder = calculateMarketOrder(modified, market, subaccount, user, isBuying, input) - val filled = parser.asBool(marketOrder?.get("filled")) ?: false - val tradeSize = parser.asNativeMap(modified["size"])?.mutable() + val marketOrder = createMarketOrder( + trade = trade, + market = market, + subaccount = subaccount, + user = user, + input = input, + ) + val filled = marketOrder?.filled ?: false + var tradeSize = TradeInputSize.safeCreate(trade.size) when (input) { - "size.size", "size.percent" -> tradeSize?.safeSet( - "usdcSize", - if (filled) parser.asDouble(marketOrder?.get("usdcSize")) else null, - ) + "size.size", "size.percent" -> + tradeSize = tradeSize.copy(usdcSize = if (filled) marketOrder?.usdcSize else null) + + "size.usdcSize" -> + tradeSize = tradeSize.copy(size = if (filled) marketOrder?.size else null) - "size.usdcSize" -> tradeSize?.safeSet( - "size", - if (filled) parser.asDouble(marketOrder?.get("size")) else null, - ) "size.leverage" -> { - tradeSize?.safeSet( - "size", - if (filled) parser.asDouble(marketOrder?.get("size")) else null, - ) - tradeSize?.safeSet( - "usdcSize", - if (filled) parser.asDouble(marketOrder?.get("usdcSize")) else null, + tradeSize = tradeSize.copy( + size = if (filled) marketOrder?.size else null, + usdcSize = if (filled) marketOrder?.usdcSize else null, ) - val orderbook = parser.asNativeMap(market?.get("orderbook_consolidated")) if (marketOrder != null && orderbook != null) { val side = side(marketOrder, orderbook) @@ -118,10 +119,11 @@ internal class TradeInputCalculatorV2( } } } - modified.safeSet("marketOrder", marketOrder) - modified.safeSet("size", tradeSize) - return modified + trade.marketOrder = marketOrder + trade.size = tradeSize + + return trade } private fun calculateClosePositionSize( @@ -159,40 +161,37 @@ internal class TradeInputCalculatorV2( return trade } - private fun calculateMarketOrder( + private fun createMarketOrder( trade: InternalTradeInputState, market: InternalMarketState?, subaccount: InternalSubaccountState?, user: InternalUserState?, input: String?, - ): Map? { + ): TradeInputMarketOrder? { val tradeSize = trade.size if (tradeSize != null) { return when (input) { "size.size", "size.percent" -> { val orderbook = getOrderbook(market = market, isBuying = trade.isBuying) - calculateMarketOrderFromSize( - parser.asDouble(tradeSize["size"]), - orderbook, + createMarketOrderFromSize( + size = tradeSize.size, + orderbook = orderbook, ) } "size.usdcSize" -> { - val stepSize = - parser.asDouble(parser.value(market, "configs.stepSize")) - ?: 0.001 - val orderbook = orderbook(market, isBuying) - calculateMarketOrderFromUsdcSize( - parser.asDouble(tradeSize["usdcSize"]), - orderbook, - stepSize, + val stepSize = market?.perpetualMarket?.configs?.stepSize ?: 0.001 + val orderbook = getOrderbook(market = market, isBuying = trade.isBuying) + createMarketOrderFromUsdcSize( + usdcSize = tradeSize.usdcSize, + orderbook = orderbook, + stepSize = stepSize, ) } "size.leverage" -> { - val leverage = - parser.asDouble(parser.value(trade, "size.leverage")) ?: return null - calculateMarketOrderFromLeverage( + val leverage = tradeSize.leverage ?: return null + createMarketOrderFromLeverage( leverage, market, subaccount, @@ -211,16 +210,16 @@ internal class TradeInputCalculatorV2( isBuying: Boolean?, ): List? { return when (isBuying) { - true -> return market?.consolidatedOrderbook?.asks - false -> return market?.consolidatedOrderbook?.bids - else -> return null + true -> market?.consolidatedOrderbook?.asks + false -> market?.consolidatedOrderbook?.bids + else -> null } } - private fun calculateMarketOrderFromSize( + private fun createMarketOrderFromSize( size: Double?, orderbook: List?, - ): Map? { + ): TradeInputMarketOrder? { return if (size != null && size != Numeric.double.ZERO) { if (orderbook != null) { val desiredSize = size @@ -230,9 +229,8 @@ internal class TradeInputCalculatorV2( var filled = false val marketOrderOrderBook = mutableListOf() orderbookLoop@ for (element in orderbook) { - val entry = element - val entryPrice = entry.price - val entrySize = entry.size + val entryPrice = element.price + val entrySize = element.size filled = (sizeTotal + entrySize >= size) @@ -243,25 +241,25 @@ internal class TradeInputCalculatorV2( usdcSizeTotal += matchedUsdcSize worstPrice = entryPrice - marketOrderOrderBook.add(entry.copy(size = matchedSize)) + marketOrderOrderBook.add(matchingOrderbookEntry(element, matchedSize)) if (filled) { break@orderbookLoop } } - marketOrder( - marketOrderOrderBook, - sizeTotal, - usdcSizeTotal, - worstPrice, - filled, + createMarketOrderWith( + orderbook = marketOrderOrderBook, + size = sizeTotal, + usdcSize = usdcSizeTotal, + worstPrice = worstPrice, + filled = filled, ) } else { - marketOrder( - mutableListOf>(), - parser.asDouble(size)!!, - Numeric.double.ZERO, - null, - false, + createMarketOrderWith( + orderbook = listOf(), + size = size, + usdcSize = Numeric.double.ZERO, + worstPrice = null, + filled = false, ) } } else { @@ -269,26 +267,278 @@ internal class TradeInputCalculatorV2( } } - private fun marketOrder( - orderbook: List>, + private fun createMarketOrderFromUsdcSize( + usdcSize: Double?, + orderbook: List?, + stepSize: Double, + ): TradeInputMarketOrder? { + return if (usdcSize != null && usdcSize != Numeric.double.ZERO) { + if (orderbook != null) { + val desiredUsdcSize = usdcSize + var sizeTotal = Numeric.double.ZERO + var usdcSizeTotal = Numeric.double.ZERO + var worstPrice: Double? = null + var filled = false + val marketOrderOrderBook = mutableListOf() + + orderbookLoop@ for (element in orderbook) { + val entryPrice = element.price + val entrySize = element.size + + if (entryPrice > Numeric.double.ZERO) { + val entryUsdcSize = entrySize * entryPrice + filled = (usdcSizeTotal + entryUsdcSize >= desiredUsdcSize) + + var matchedSize = entrySize + var matchedUsdcSize = entryUsdcSize + if (filled) { + matchedUsdcSize = desiredUsdcSize - usdcSizeTotal + matchedSize = matchedUsdcSize / entryPrice + matchedSize = + Rounder.quickRound( + matchedSize, + stepSize, + ) + matchedUsdcSize = matchedSize * entryPrice + } + sizeTotal += matchedSize + usdcSizeTotal += matchedUsdcSize + + worstPrice = entryPrice + marketOrderOrderBook.add(matchingOrderbookEntry(element, matchedSize)) + if (filled) { + break@orderbookLoop + } + } + } + createMarketOrderWith( + orderbook = marketOrderOrderBook, + size = sizeTotal, + usdcSize = usdcSizeTotal, + worstPrice = worstPrice, + filled = filled, + ) + } else { + createMarketOrderWith( + orderbook = listOf(), + size = Numeric.double.ZERO, + usdcSize = usdcSize, + worstPrice = null, + filled = false, + ) + } + } else { + null + } + } + + private fun createMarketOrderFromLeverage( + leverage: Double, + market: InternalMarketState?, + subaccount: InternalSubaccountState?, + user: InternalUserState?, + ): TradeInputMarketOrder? { + val stepSize = market?.perpetualMarket?.configs?.stepSize ?: 0.001 + val equity = subaccount?.calculated?.get(CalculationPeriod.current)?.equity ?: return null + val oraclePrice = market?.perpetualMarket?.oraclePrice ?: return null + val feeRate = user?.takerFeeRate ?: Numeric.double.ZERO + val positions = subaccount.openPositions + val marketId = market.perpetualMarket?.id + val positionSize = if (positions != null && marketId != null) { + positions[marketId]?.calculated?.get(CalculationPeriod.current)?.size + } else { + null + } + return if (equity > Numeric.double.ZERO) { + val existingLeverage = + ((positionSize ?: Numeric.double.ZERO) * oraclePrice) / equity + val calculatedIsBuying = + if (leverage > existingLeverage) { + true + } else if (leverage < existingLeverage) { + false + } else { + null + } + if (calculatedIsBuying != null) { + val orderbook = getOrderbook(market, calculatedIsBuying) + if (orderbook != null) { + createMarketOrderFromLeverageWith( + equity = equity, + oraclePrice = oraclePrice, + positionSize = positionSize, + isBuying = calculatedIsBuying, + feeRate = feeRate, + leverage = leverage, + stepSize = stepSize, + orderbook = orderbook, + ) + } else { + null + } + } else { + null + } + } else { + null + } + } + + private fun createMarketOrderFromLeverageWith( + equity: Double, + oraclePrice: Double, + positionSize: Double?, + isBuying: Boolean, + feeRate: Double, + leverage: Double, + stepSize: Double, + orderbook: List, + ): TradeInputMarketOrder? { + /* + leverage = (size * oracle_price) / account_equity + leverage and size are signed + + new_account_equity = old_account_equity + order_size * (oracle_price - market_price) - abs(order_size) * market_price * fee rate + order_size is signed + + (old_size + order_size) * oracle_price = leverage * (old_account_equity + order_size * (oracle_price - market_price) - abs(order_size) * market_price * fee_rate) + + X = order_size + SZ = old_size + OR = oracle_price + AE = account_equity + MP = market price + FR = fee rate + LV = leverage + PS = positionSign LONG ? 1 : -1 + OS = orderSign BUY ? 1 : -1 + + (SZ + X) * OR = LV * (AE + X * (OR - MP) - OS * X * MP * FR) + SZ * OR + OR * X = LV * AE + LV * X * (OR - MP) - OS * LV * MP * FR * X + OR * X + OS * LV * MP * FR * X - LV * X * (OR - MP) = LV * AE - SZ * OR + X = (LV * AE - SZ * OR) / (OR + OS * LV * MP * FR - LV * (OR - MP)) + X = (LV * AE - SZ * OR) / (OR + OS * LV * MP * FR - LV * (OR - MP)) + + new(AE) = AE + X * (OR - MP) - abs(X) * MP * FR + */ + var sizeTotal = Numeric.double.ZERO + var usdcSizeTotal = Numeric.double.ZERO + var worstPrice: Double? = null + var filled = false + val marketOrderOrderBook = mutableListOf() + + /* + Breaking naming rules a little bit to match the documentation above + */ + @Suppress("LocalVariableName") + val OR = oraclePrice + + @Suppress("LocalVariableName") + val LV = leverage + + @Suppress("LocalVariableName") + val OS: Double = + if (isBuying) Numeric.double.POSITIVE else Numeric.double.NEGATIVE + + @Suppress("LocalVariableName") + val FR = feeRate + + @Suppress("LocalVariableName") + var AE = equity + + @Suppress("LocalVariableName") + var SZ = positionSize ?: Numeric.double.ZERO + + orderbookLoop@ for (element in orderbook) { + + val entryPrice = element.price + val entrySize = element.size + if (entryPrice != Numeric.double.ZERO) { + + val MP = entryPrice + + @Suppress("LocalVariableName") + val X = ((LV * AE) - (SZ * OR)) / + (OR + (OS * LV * MP * FR) - (LV * (OR - MP))) + val desiredSize = X.abs() + if (desiredSize < entrySize) { + val rounded = this.rounded(sizeTotal, desiredSize, stepSize) + sizeTotal = Rounder.quickRound(sizeTotal + rounded, stepSize) + usdcSizeTotal += rounded * MP + worstPrice = entryPrice + filled = true + marketOrderOrderBook.add(matchingOrderbookEntry(element, rounded)) + } else { + val rounded = this.rounded(sizeTotal, entrySize, stepSize) + sizeTotal = Rounder.quickRound(sizeTotal + rounded, stepSize) + usdcSizeTotal += rounded * MP + /* + new(AE) = AE + X * (OR - MP) - abs(X) * MP * FR + */ + var signedSize = rounded + if (!isBuying) { + signedSize *= exchange.dydx.abacus.utils.Numeric.double.NEGATIVE + } + AE = AE + (signedSize * (OR - MP)) - (rounded * MP * FR) + SZ += signedSize + marketOrderOrderBook.add(matchingOrderbookEntry(element, rounded)) + } + } + + if (filled) { + break@orderbookLoop + } + } + return createMarketOrderWith( + orderbook = marketOrderOrderBook, + size = sizeTotal, + usdcSize = usdcSizeTotal, + worstPrice = worstPrice, + filled = filled, + ) + } + + private fun createMarketOrderWith( + orderbook: List, size: Double?, usdcSize: Double?, worstPrice: Double?, filled: Boolean, - ): Map? { - return if (size != null && usdcSize != null) { - val marketOrder = exchange.dydx.abacus.utils.mutableMapOf() - marketOrder.safeSet("orderbook", orderbook) - if (size != Numeric.double.ZERO) { - marketOrder.safeSet("price", (usdcSize / size)) - } - marketOrder.safeSet("size", size) - marketOrder.safeSet("usdcSize", usdcSize) - marketOrder.safeSet("worstPrice", worstPrice) - marketOrder.safeSet("filled", filled) - marketOrder + ): TradeInputMarketOrder? { + return if (size != null && usdcSize != null && size != Numeric.double.ZERO) { + TradeInputMarketOrder( + orderbook = orderbook.map { + OrderbookUsage( + price = it.price, + size = it.size, + ) + }.toIList(), + price = (usdcSize / size), + size = size, + usdcSize = usdcSize, + worstPrice = worstPrice, + filled = filled, + ) } else { null } } + + private fun matchingOrderbookEntry( + entry: InternalOrderbookTick, + size: Double, + ): InternalOrderbookTick { + return entry.copy(size = size) + } + + private fun rounded( + sizeTotal: Double, + desiredSize: Double, + stepSize: Double, + ): Double { + val desiredTotal = sizeTotal + desiredSize + val rounded = Rounder.quickRound(desiredTotal, stepSize) + return rounded - sizeTotal + } + } \ No newline at end of file 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 950375bf7..e4dbab884 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/output/input/TradeInput.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/TradeInput.kt @@ -404,7 +404,6 @@ data class TradeInputSummary( } } -@Suppress("UNCHECKED_CAST") @JsExport @Serializable data class OrderbookUsage( diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt index 73a5429dd..98f83eb9b 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt @@ -28,6 +28,7 @@ import exchange.dydx.abacus.output.input.Tooltip import exchange.dydx.abacus.output.input.TradeInputBracket import exchange.dydx.abacus.output.input.TradeInputBracketSide import exchange.dydx.abacus.output.input.TradeInputGoodUntil +import exchange.dydx.abacus.output.input.TradeInputMarketOrder import exchange.dydx.abacus.output.input.TradeInputPrice import exchange.dydx.abacus.output.input.TradeInputSize import exchange.dydx.abacus.state.manager.HistoricalTradingRewardsPeriod @@ -74,9 +75,9 @@ internal data class InternalTradeInputState( var reduceOnly: Boolean = false, var postOnly: Boolean = false, var fee: Double? = null, - var bracket: TradeInputBracket? = null, var options: InternalTradeInputOptions = InternalTradeInputOptions(), + var marketOrder: TradeInputMarketOrder? = null, ) { val isBuying: Boolean get() = side == OrderSide.Buy || side == null From 3eb966a1c6408917313e4a35069c040ecb9a807f Mon Sep 17 00:00:00 2001 From: Rui Date: Mon, 12 Aug 2024 19:43:13 -0700 Subject: [PATCH 27/63] WIP --- .../calculator/MarginCalculator.kt | 17 + .../calculator/TradeInputCalculator.kt | 10 +- .../V2/TradeInput/TradeInputCalculatorV2.kt | 128 ++++ .../TradeInputMarginModeCalculator.kt | 14 +- .../TradeInputMarketOrderCalculator.kt} | 125 ++-- .../TradeInputNonMarketOrderCalculator.kt | 94 +++ .../TradeInput/TradeInputOptionsCalculator.kt | 610 ++++++++++++++++++ .../TradeInput/TradeInputSummaryCalculator.kt | 481 ++++++++++++++ .../output/input/TradeInput.kt | 2 +- .../state/internalstate/InternalState.kt | 18 + .../exchange.dydx.abacus/payload/BaseTests.kt | 2 +- 11 files changed, 1402 insertions(+), 99 deletions(-) create mode 100644 src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputCalculatorV2.kt rename src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/{ => TradeInput}/TradeInputMarginModeCalculator.kt (87%) rename src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/{TradeInputCalculatorV2.kt => TradeInput/TradeInputMarketOrderCalculator.kt} (84%) create mode 100644 src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputNonMarketOrderCalculator.kt create mode 100644 src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputOptionsCalculator.kt create mode 100644 src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputSummaryCalculator.kt diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginCalculator.kt index c0e4032f6..5035524ce 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginCalculator.kt @@ -10,6 +10,7 @@ import exchange.dydx.abacus.output.input.OrderStatus import exchange.dydx.abacus.output.input.TradeInput import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.internalstate.InternalAccountState +import exchange.dydx.abacus.state.internalstate.InternalMarketState import exchange.dydx.abacus.state.internalstate.InternalPerpetualPosition import exchange.dydx.abacus.state.internalstate.InternalSubaccountState import exchange.dydx.abacus.state.internalstate.InternalTradeInputState @@ -187,6 +188,22 @@ internal object MarginCalculator { } fun selectableMarginModes( + account: InternalAccountState, + market: InternalMarketState?, + subaccountNumber: Int, + ): Boolean { + val marketId = market?.perpetualMarket?.id + val existingMarginMode = findExistingMarginMode(account, marketId, subaccountNumber) + return if (existingMarginMode != null) { + false + } else if (marketId != null) { + findMarketMarginMode(market?.perpetualMarket) == MarginMode.Cross + } else { + true + } + } + + fun selectableMarginModesDeprecated( parser: ParserProtocol, account: Map?, market: Map?, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt index 8387456c7..4c56a8348 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInputCalculator.kt @@ -32,7 +32,7 @@ enum class TradeCalculation(val rawValue: String) { companion object { operator fun invoke(rawValue: String) = - entries.firstOrNull { it.rawValue == rawValue } + TradeCalculation.values().firstOrNull { it.rawValue == rawValue } } } @@ -1120,7 +1120,7 @@ internal class TradeInputCalculator( account: Map?, subaccount: Map? ): Map? { - val selectableMarginMode = MarginCalculator.selectableMarginModes( + val selectableMarginMode = MarginCalculator.selectableMarginModesDeprecated( parser = parser, account = account, market = market, @@ -1414,11 +1414,7 @@ internal class TradeInputCalculator( tokenPrice > 0.0 ) { val feeMultiplier = feeMultiplierPpm / QUANTUM_MULTIPLIER - return feeMultiplier * (fee - maxMakerRebate * notional) / ( - tokenPrice * 10.0.pow( - tokenPriceExponent, - ) - ) + return feeMultiplier * (fee - maxMakerRebate * notional) / (tokenPrice * 10.0.pow(tokenPriceExponent)) } return null } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputCalculatorV2.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputCalculatorV2.kt new file mode 100644 index 000000000..4defca71c --- /dev/null +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputCalculatorV2.kt @@ -0,0 +1,128 @@ +package exchange.dydx.abacus.calculator.v2.TradeInput + +import exchange.dydx.abacus.calculator.TradeCalculation +import exchange.dydx.abacus.output.FeeTier +import exchange.dydx.abacus.output.input.OrderType +import exchange.dydx.abacus.protocols.ParserProtocol +import exchange.dydx.abacus.state.internalstate.InternalAccountState +import exchange.dydx.abacus.state.internalstate.InternalConfigsState +import exchange.dydx.abacus.state.internalstate.InternalMarketState +import exchange.dydx.abacus.state.internalstate.InternalMarketSummaryState +import exchange.dydx.abacus.state.internalstate.InternalRewardsParamsState +import exchange.dydx.abacus.state.internalstate.InternalSubaccountState +import exchange.dydx.abacus.state.internalstate.InternalTradeInputState +import exchange.dydx.abacus.state.internalstate.InternalUserState +import exchange.dydx.abacus.state.internalstate.InternalWalletState + +internal class TradeInputCalculatorV2( + private val parser: ParserProtocol, + private val calculation: TradeCalculation, + private val marginModeCalculator: TradeInputMarginModeCalculator = TradeInputMarginModeCalculator(), + private val marketOrderCalculator: TradeInputMarketOrderCalculator = TradeInputMarketOrderCalculator(calculation), + private val nonMarketOrderCalculator: TradeInputNonMarketOrderCalculator = TradeInputNonMarketOrderCalculator(), + private val optionsCalculator: TradeInputOptionsCalculator = TradeInputOptionsCalculator(parser), + private val summaryCalculator: TradeInputSummaryCalculator = TradeInputSummaryCalculator(), +) { + fun calculate( + trade: InternalTradeInputState, + wallet: InternalWalletState, + marketSummary: InternalMarketSummaryState, + rewardsParams: InternalRewardsParamsState?, + configs: InternalConfigsState, + subaccountNumber: Int, + input: String?, + ): InternalTradeInputState { + val account = wallet.account + val subaccount = + account.groupedSubaccounts[subaccountNumber] ?: account.subaccounts[subaccountNumber] + val user = wallet.user + val markets = marketSummary.markets + + marginModeCalculator.updateTradeInputMarginMode( + tradeInput = trade, + markets = markets, + account = account, + subaccountNumber = subaccountNumber, + ) + + if (input != null) { + when (trade.type) { + OrderType.Market, + OrderType.StopMarket, + OrderType.TakeProfitMarket -> + marketOrderCalculator.calculate( + trade = trade, + market = markets[trade.marketId], + subaccount = subaccount, + user = user, + input = input, + ) + + OrderType.Limit, + OrderType.StopLimit, + OrderType.TakeProfitLimit, + OrderType.TrailingStop, + OrderType.Liquidated, + OrderType.Liquidation, + OrderType.Offsetting, + OrderType.Deleveraged, + OrderType.FinalSettlement, + null -> + nonMarketOrderCalculator.calculate( + trade = trade, + market = markets[trade.marketId], + input = input, + ) + } + } + + finalize( + trade = trade, + account = account, + subaccount = subaccount, + user = user, + market = markets[trade.marketId], + rewardsParams = rewardsParams, + feeTiers = configs.feeTiers, + ) + + return trade + } + + private fun finalize( + trade: InternalTradeInputState, + account: InternalAccountState, + subaccount: InternalSubaccountState?, + user: InternalUserState?, + market: InternalMarketState?, + rewardsParams: InternalRewardsParamsState?, + feeTiers: List?, + ): InternalTradeInputState { + val type = trade.type + val marketId = market?.perpetualMarket?.id + val position = if (marketId != null) { + subaccount?.openPositions?.get(marketId) + } else { + null + } + + optionsCalculator.calculate( + trade = trade, + position = position, + account = account, + subaccount = subaccount, + market = market, + ) + + summaryCalculator.calculate( + trade = trade, + subaccount = subaccount, + user = user, + market = market, + rewardsParams = rewardsParams, + feeTiers = feeTiers, + ) + + return trade + } +} diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInputMarginModeCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputMarginModeCalculator.kt similarity index 87% rename from src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInputMarginModeCalculator.kt rename to src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputMarginModeCalculator.kt index 7ef843c7b..2e2c37910 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInputMarginModeCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputMarginModeCalculator.kt @@ -1,4 +1,4 @@ -package exchange.dydx.abacus.calculator.v2 +package exchange.dydx.abacus.calculator.v2.TradeInput import exchange.dydx.abacus.calculator.CalculationPeriod import exchange.dydx.abacus.calculator.MarginCalculator @@ -15,15 +15,15 @@ internal class TradeInputMarginModeCalculator { subaccountNumber: Int, ): InternalTradeInputState { val existingMarginMode = MarginCalculator.findExistingMarginMode( - account = account, - marketId = tradeInput.marketId, - subaccountNumber = subaccountNumber, - ) + account = account, + marketId = tradeInput.marketId, + subaccountNumber = subaccountNumber, + ) // If there is an existing position or order, we have to use the same margin mode if (existingMarginMode != null) { tradeInput.marginMode = existingMarginMode - if ( existingMarginMode == MarginMode.Isolated && tradeInput.targetLeverage == null) { + if (existingMarginMode == MarginMode.Isolated && tradeInput.targetLeverage == null) { val existingPosition = MarginCalculator.findExistingPosition( account = account, marketId = tradeInput.marketId, @@ -51,4 +51,4 @@ internal class TradeInputMarginModeCalculator { } return tradeInput } -} \ No newline at end of file +} diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInputCalculatorV2.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputMarketOrderCalculator.kt similarity index 84% rename from src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInputCalculatorV2.kt rename to src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputMarketOrderCalculator.kt index 479e19e17..cdbecc1d5 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInputCalculatorV2.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputMarketOrderCalculator.kt @@ -1,84 +1,30 @@ -package exchange.dydx.abacus.calculator.v2 +@file:Suppress("ktlint:standard:property-naming") + +package exchange.dydx.abacus.calculator.v2.TradeInput import abs import exchange.dydx.abacus.calculator.CalculationPeriod import exchange.dydx.abacus.calculator.TradeCalculation import exchange.dydx.abacus.output.input.OrderSide -import exchange.dydx.abacus.output.input.OrderType import exchange.dydx.abacus.output.input.OrderbookUsage import exchange.dydx.abacus.output.input.TradeInputMarketOrder import exchange.dydx.abacus.output.input.TradeInputSize import exchange.dydx.abacus.state.internalstate.InternalMarketState -import exchange.dydx.abacus.state.internalstate.InternalMarketSummaryState +import exchange.dydx.abacus.state.internalstate.InternalOrderbook import exchange.dydx.abacus.state.internalstate.InternalOrderbookTick -import exchange.dydx.abacus.state.internalstate.InternalRewardsParamsState import exchange.dydx.abacus.state.internalstate.InternalSubaccountState import exchange.dydx.abacus.state.internalstate.InternalTradeInputState import exchange.dydx.abacus.state.internalstate.InternalUserState -import exchange.dydx.abacus.state.internalstate.InternalWalletState import exchange.dydx.abacus.state.internalstate.safeCreate import exchange.dydx.abacus.state.model.ClosePositionInputField import exchange.dydx.abacus.utils.Numeric import exchange.dydx.abacus.utils.Rounder -import exchange.dydx.abacus.utils.mutable -import exchange.dydx.abacus.utils.safeSet import kollections.toIList -internal class TradeInputCalculatorV2( +internal class TradeInputMarketOrderCalculator( private val calculation: TradeCalculation, - private val marginModeCalculator: TradeInputMarginModeCalculator = TradeInputMarginModeCalculator(), ) { fun calculate( - trade: InternalTradeInputState, - wallet: InternalWalletState, - marketSummary: InternalMarketSummaryState, - rewardsParams: InternalRewardsParamsState?, - subaccountNumber: Int, - input: String?, - ): InternalTradeInputState { - val account = wallet.account - val subaccount = - account.groupedSubaccounts[subaccountNumber] ?: account.subaccounts[subaccountNumber] - val user = wallet.user - val markets = marketSummary.markets - - marginModeCalculator.updateTradeInputMarginMode( - tradeInput = trade, - markets = markets, - account = account, - subaccountNumber = subaccountNumber, - ) - - if (input != null) { - when (trade.type) { - OrderType.Market, - OrderType.StopMarket, - OrderType.TakeProfitMarket -> - calculateMarketOrderTrade( - trade = trade, - market = markets[trade.marketId], - subaccount = subaccount, - user = user, - input = input, - ) - - OrderType.Limit -> TODO() - OrderType.StopLimit -> TODO() - OrderType.TakeProfitLimit -> TODO() - OrderType.TrailingStop -> TODO() - OrderType.Liquidated -> TODO() - OrderType.Liquidation -> TODO() - OrderType.Offsetting -> TODO() - OrderType.Deleveraged -> TODO() - OrderType.FinalSettlement -> TODO() - null -> TODO() - } - } - - return trade - } - - private fun calculateMarketOrderTrade( trade: InternalTradeInputState, market: InternalMarketState?, subaccount: InternalSubaccountState?, @@ -91,30 +37,29 @@ internal class TradeInputCalculatorV2( val marketOrder = createMarketOrder( trade = trade, market = market, - subaccount = subaccount, - user = user, - input = input, + subaccount = subaccount, + user = user, + input = input, ) val filled = marketOrder?.filled ?: false var tradeSize = TradeInputSize.safeCreate(trade.size) when (input) { "size.size", "size.percent" -> - tradeSize = tradeSize.copy(usdcSize = if (filled) marketOrder?.usdcSize else null) + tradeSize = tradeSize.copy(usdcSize = if (filled) marketOrder?.usdcSize else null) "size.usdcSize" -> tradeSize = tradeSize.copy(size = if (filled) marketOrder?.size else null) - "size.leverage" -> { tradeSize = tradeSize.copy( size = if (filled) marketOrder?.size else null, usdcSize = if (filled) marketOrder?.usdcSize else null, ) - val orderbook = parser.asNativeMap(market?.get("orderbook_consolidated")) + val orderbook = market?.consolidatedOrderbook if (marketOrder != null && orderbook != null) { - val side = side(marketOrder, orderbook) - if (side != null && side != parser.asString(modified["side"])) { - modified.safeSet("side", side) + val side = calculateSide(marketOrder, orderbook) + if (side != null && side != trade.side) { + trade.side = side } } } @@ -126,6 +71,22 @@ internal class TradeInputCalculatorV2( return trade } + private fun calculateSide( + marketOrder: TradeInputMarketOrder, + orderbook: InternalOrderbook, + ): OrderSide? { + val firstMarketOrderbookPrice = marketOrder.orderbook?.firstOrNull()?.price ?: return null + val firstAskPrice = orderbook.asks?.firstOrNull()?.price ?: return null + val firstBidPrice = orderbook.bids?.firstOrNull()?.price ?: return null + return if (firstMarketOrderbookPrice == firstAskPrice) { + OrderSide.Buy + } else if (firstMarketOrderbookPrice == firstBidPrice) { + OrderSide.Sell + } else { + null + } + } + private fun calculateClosePositionSize( trade: InternalTradeInputState, market: InternalMarketState?, @@ -174,7 +135,7 @@ internal class TradeInputCalculatorV2( "size.size", "size.percent" -> { val orderbook = getOrderbook(market = market, isBuying = trade.isBuying) createMarketOrderFromSize( - size = tradeSize.size, + size = tradeSize.size, orderbook = orderbook, ) } @@ -184,7 +145,7 @@ internal class TradeInputCalculatorV2( val orderbook = getOrderbook(market = market, isBuying = trade.isBuying) createMarketOrderFromUsdcSize( usdcSize = tradeSize.usdcSize, - orderbook = orderbook, + orderbook = orderbook, stepSize = stepSize, ) } @@ -274,7 +235,7 @@ internal class TradeInputCalculatorV2( ): TradeInputMarketOrder? { return if (usdcSize != null && usdcSize != Numeric.double.ZERO) { if (orderbook != null) { - val desiredUsdcSize = usdcSize + val desiredUsdcSize = usdcSize var sizeTotal = Numeric.double.ZERO var usdcSizeTotal = Numeric.double.ZERO var worstPrice: Double? = null @@ -430,36 +391,35 @@ internal class TradeInputCalculatorV2( /* Breaking naming rules a little bit to match the documentation above */ - @Suppress("LocalVariableName") + @Suppress("LocalVariableName", "PropertyName") val OR = oraclePrice - @Suppress("LocalVariableName") + @Suppress("LocalVariableName", "PropertyName") val LV = leverage - @Suppress("LocalVariableName") + @Suppress("LocalVariableName", "PropertyName") val OS: Double = if (isBuying) Numeric.double.POSITIVE else Numeric.double.NEGATIVE - @Suppress("LocalVariableName") + @Suppress("LocalVariableName", "PropertyName") val FR = feeRate - @Suppress("LocalVariableName") + @Suppress("LocalVariableName", "PropertyName") var AE = equity - @Suppress("LocalVariableName") + @Suppress("LocalVariableName", "PropertyName") var SZ = positionSize ?: Numeric.double.ZERO orderbookLoop@ for (element in orderbook) { - val entryPrice = element.price val entrySize = element.size if (entryPrice != Numeric.double.ZERO) { - + @Suppress("LocalVariableName", "PropertyName") val MP = entryPrice - @Suppress("LocalVariableName") + @Suppress("LocalVariableName", "PropertyName") val X = ((LV * AE) - (SZ * OR)) / - (OR + (OS * LV * MP * FR) - (LV * (OR - MP))) + (OR + (OS * LV * MP * FR) - (LV * (OR - MP))) val desiredSize = X.abs() if (desiredSize < entrySize) { val rounded = this.rounded(sizeTotal, desiredSize, stepSize) @@ -540,5 +500,4 @@ internal class TradeInputCalculatorV2( val rounded = Rounder.quickRound(desiredTotal, stepSize) return rounded - sizeTotal } - -} \ No newline at end of file +} diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputNonMarketOrderCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputNonMarketOrderCalculator.kt new file mode 100644 index 000000000..e785c1c39 --- /dev/null +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputNonMarketOrderCalculator.kt @@ -0,0 +1,94 @@ +package exchange.dydx.abacus.calculator.v2.TradeInput + +import exchange.dydx.abacus.output.input.OrderType +import exchange.dydx.abacus.output.input.TradeInputPrice +import exchange.dydx.abacus.state.internalstate.InternalMarketState +import exchange.dydx.abacus.state.internalstate.InternalTradeInputState +import exchange.dydx.abacus.utils.Numeric +import exchange.dydx.abacus.utils.Rounder + +internal class TradeInputNonMarketOrderCalculator { + fun calculate( + trade: InternalTradeInputState, + market: InternalMarketState?, + input: String, + ): InternalTradeInputState { + var tradeSize = trade.size + if (tradeSize != null) { + val isBuying = trade.isBuying + val tradePrices = trade.price + val stepSize = market?.perpetualMarket?.configs?.stepSize ?: 0.001 + val price = getNonMarketOrderPrice(tradePrices, market, trade.type, isBuying) + when (input) { + "size.size" -> { + val size = tradeSize.size + val usdcSize = + if (price != null && size != null) (price * size) else null + tradeSize = tradeSize.copy(usdcSize = usdcSize) + } + + "size.usdcSize" -> { + val usdcSize = tradeSize.usdcSize + val size = + if (price != null && usdcSize != null && usdcSize > Numeric.double.ZERO && price > Numeric.double.ZERO) { + Rounder.round(usdcSize / price, stepSize) + } else { + null + } + tradeSize = tradeSize.copy(size = size) + } + + else -> { + val size = tradeSize.size + val usdcSize = + if (price != null && size != null) (price * size) else null + tradeSize = tradeSize.copy(usdcSize = usdcSize) + } + } + trade.size = tradeSize + } + + trade.marketOrder = null + return trade + } + + private fun getNonMarketOrderPrice( + prices: TradeInputPrice?, + market: InternalMarketState?, + type: OrderType?, + isBuying: Boolean, + ): Double? { + if (prices == null) { + return null + } + when (type) { + OrderType.Limit, OrderType.StopLimit, OrderType.TakeProfitLimit -> { + return prices.limitPrice + } + + OrderType.TrailingStop -> { + val oraclePrice = market?.perpetualMarket?.oraclePrice ?: return null + + val trailingPercent = prices.trailingPercent ?: Numeric.double.ZERO + if (trailingPercent != Numeric.double.ZERO) { + val percent = + if (isBuying) { + (Numeric.double.ONE - trailingPercent) + } else { + (Numeric.double.ONE + trailingPercent) + } + return oraclePrice * percent + } + return null + } + + OrderType.StopMarket, OrderType.TakeProfitMarket -> { + return prices.triggerPrice + } + + else -> { + return null + } + } + } +} diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputOptionsCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputOptionsCalculator.kt new file mode 100644 index 000000000..7a8408cdc --- /dev/null +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputOptionsCalculator.kt @@ -0,0 +1,610 @@ +package exchange.dydx.abacus.calculator.v2.TradeInput + +import exchange.dydx.abacus.calculator.CalculationPeriod +import exchange.dydx.abacus.calculator.MarginCalculator +import exchange.dydx.abacus.output.input.MarginMode +import exchange.dydx.abacus.output.input.OrderType +import exchange.dydx.abacus.output.input.SelectionOption +import exchange.dydx.abacus.output.input.Tooltip +import exchange.dydx.abacus.output.input.TradeInputGoodUntil +import exchange.dydx.abacus.protocols.ParserProtocol +import exchange.dydx.abacus.state.internalstate.InternalAccountState +import exchange.dydx.abacus.state.internalstate.InternalMarketState +import exchange.dydx.abacus.state.internalstate.InternalPerpetualPosition +import exchange.dydx.abacus.state.internalstate.InternalSubaccountState +import exchange.dydx.abacus.state.internalstate.InternalTradeInputOptions +import exchange.dydx.abacus.state.internalstate.InternalTradeInputState +import exchange.dydx.abacus.state.internalstate.safeCreate + +internal class TradeInputOptionsCalculator( + private val parser: ParserProtocol, +) { + fun calculate( + trade: InternalTradeInputState, + account: InternalAccountState, + subaccount: InternalSubaccountState?, + market: InternalMarketState?, + position: InternalPerpetualPosition?, + ): InternalTradeInputState { + trade.options = calculatedOptions( + account = account, + subaccount = subaccount, + trade = trade, + position = position, + market = market, + ) + applyDefaultOptions( + trade = trade, + account = account, + subaccount = subaccount, + position = position, + market = market, + ) + + return trade + } + + private fun calculatedOptions( + account: InternalAccountState, + subaccount: InternalSubaccountState?, + trade: InternalTradeInputState, + position: InternalPerpetualPosition?, + market: InternalMarketState?, + ): InternalTradeInputOptions { + val fields = requiredFields( + trade = trade, + account = account, + subaccount = subaccount, + market = market, + ) + + return calculatedOptionsFromFields( + fields = fields, + trade = trade, + position = position, + market = market, + ) + } + + private fun requiredFields( + trade: InternalTradeInputState, + account: InternalAccountState, + subaccount: InternalSubaccountState?, + market: InternalMarketState?, + ): List? { + return when (trade.type) { + OrderType.Market -> { + return when (trade.marginMode) { + MarginMode.Isolated -> listOf( + sizeField(), + bracketsField(), + marginModeField(market, account, subaccount), + reduceOnlyField(), + ).filterNotNull() + + else -> listOf( + sizeField(), + leverageField(), + bracketsField(), + marginModeField(market, account, subaccount), + reduceOnlyField(), + ).filterNotNull() + } + } + + OrderType.Limit -> { + when (trade.timeInForce) { + "GTT" -> + listOf( + sizeField(), + limitPriceField(), + timeInForceField(), + goodTilField(), + postOnlyField(), + marginModeField(market, account, subaccount), + ).filterNotNull() + + else -> + listOf( + sizeField(), + limitPriceField(), + timeInForceField(), + marginModeField(market, account, subaccount), + reduceOnlyField(), + ).filterNotNull() + } + } + + OrderType.StopLimit, OrderType.TakeProfitLimit -> { + val execution = trade.execution + listOf( + sizeField(), + limitPriceField(), + triggerPriceField(), + goodTilField(), + executionField(true), + marginModeField(market, account, subaccount), + when (execution) { + "IOC" -> reduceOnlyField() + else -> null + }, + ).filterNotNull() + } + + OrderType.StopMarket, OrderType.TakeProfitMarket -> { + listOf( + sizeField(), + triggerPriceField(), + goodTilField(), + executionField(false), + marginModeField(market, account, subaccount), + reduceOnlyField(), + ).filterNotNull() + } + + OrderType.TrailingStop -> { + listOf( + sizeField(), + trailingPercentField(), + goodTilField(), + executionField(false), + marginModeField(market, account, subaccount), + ).filterNotNull() + } + + OrderType.Liquidated, + OrderType.Liquidation, + OrderType.Offsetting, + OrderType.Deleveraged, + OrderType.FinalSettlement, + null -> null + } + } + + private fun calculatedOptionsFromFields( + fields: List?, + trade: InternalTradeInputState, + position: InternalPerpetualPosition?, + market: InternalMarketState?, + ): InternalTradeInputOptions { + if (fields == null) { + return trade.options + } + val options = InternalTradeInputOptions() + for (item in fields) { + parser.asNativeMap(item)?.let { field -> + when (parser.asString(field["field"])) { + "size.size" -> options.needsSize = true + "size.leverage" -> options.needsLeverage = true + "price.triggerPrice" -> options.needsTriggerPrice = true + "price.limitPrice" -> options.needsLimitPrice = true + "price.trailingPercent" -> options.needsTrailingPercent = true + "timeInForce" -> { + options.timeInForceOptions = field["options"] as? List + // options.needsTimeInForce = true + } + + "goodTil" -> { + options.goodTilUnitOptions = goodTilUnitField()["options"] as? List + options.needsGoodUntil = true + } + + "execution" -> { + options.executionOptions = field["options"] as? List + // options.needsExecution = true + } + + "marginMode" -> { + options.marginModeOptions = field["options"] as? List + options.needsMarginMode = true + } + + "reduceOnly" -> options.needsReduceOnly = true + "postOnly" -> options.needsPostOnly = true + "brackets" -> options.needsBrackets = true + } + } + } + + if (options.needsLeverage) { + options.maxLeverage = maxLeverageFromPosition(position, market) + } else { + options.maxLeverage = null + } + + if (options.needsReduceOnly) { + options.reduceOnlyTooltip = null + } else { + options.reduceOnlyTooltip = buildToolTip(reduceOnlyPromptFromTrade(trade.type)) + } + + if (options.needsPostOnly) { + options.postOnlyTooltip = null + } else { + options.postOnlyTooltip = buildToolTip(postOnlyPromptFromTrade(trade.type)) + } + + options.needsTargetLeverage = trade.marginMode == MarginMode.Isolated + + return options + } + + private fun applyDefaultOptions( + trade: InternalTradeInputState, + account: InternalAccountState, + subaccount: InternalSubaccountState?, + position: InternalPerpetualPosition?, + market: InternalMarketState?, + ): InternalTradeInputState { + var options = calculatedOptions( + account = account, + subaccount = subaccount, + trade = trade, + position = position, + market = market, + ) + if (options.timeInForceOptions != null) { + if (trade.timeInForce == null) { + trade.timeInForce = options.timeInForceOptions?.firstOrNull()?.type + } + } + + options = calculatedOptions( + account = account, + subaccount = subaccount, + trade = trade, + position = position, + market = market, + ) + if (options.goodTilUnitOptions != null) { + if (trade.goodTil?.unit == null) { + val goodTil = TradeInputGoodUntil.safeCreate(trade.goodTil) + trade.goodTil = goodTil.copy(unit = "D") + } + } + + options = calculatedOptions( + account = account, + subaccount = subaccount, + trade = trade, + position = position, + market = market, + ) + if (options.executionOptions != null) { + if (trade.execution == null) { + trade.execution = options.executionOptions?.firstOrNull()?.type + } + } + + options = calculatedOptions( + account = account, + subaccount = subaccount, + trade = trade, + position = position, + market = market, + ) + if (options.marginModeOptions != null) { + if (trade.marginMode == null) { + trade.marginMode = MarginMode.invoke(options.marginModeOptions?.firstOrNull()?.type) + } + } + + options = calculatedOptions( + account = account, + subaccount = subaccount, + trade = trade, + position = position, + market = market, + ) + if (options.needsGoodUntil) { + if (trade.goodTil == null) { + val goodTil = TradeInputGoodUntil.safeCreate(trade.goodTil) + trade.goodTil = goodTil.copy(duration = 28.0) + } + } + + return trade + } + + private fun buildToolTip(stringKey: String?): Tooltip? { + return if (stringKey != null) { + Tooltip(titleStringKey = "$stringKey.TITLE", bodyStringKey = "$stringKey.BODY") + } else { + null + } + } + + private fun reduceOnlyPromptFromTrade( + orderType: OrderType? + ): String? { + return when (orderType) { + OrderType.Limit -> "GENERAL.TRADE.REDUCE_ONLY_TIMEINFORCE_IOC" + OrderType.StopLimit, OrderType.TakeProfitLimit -> "GENERAL.TRADE.REDUCE_ONLY_TIMEINFORCE_IOC" + else -> return null + } + } + + private fun postOnlyPromptFromTrade( + orderType: OrderType? + ): String? { + return when (orderType) { + OrderType.Limit -> "GENERAL.TRADE.POST_ONLY_TIMEINFORCE_GTT" + else -> return null + } + } + + private fun maxLeverageFromPosition( + position: InternalPerpetualPosition?, + market: InternalMarketState?, + ): Double? { + if (position != null) { + return position.calculated[CalculationPeriod.current]?.maxLeverage + } else { + val initialMarginFraction = + market?.perpetualMarket?.configs?.effectiveInitialMarginFraction + ?: return null + return 1.0 / initialMarginFraction + } + } + + private fun marginModeField( + market: InternalMarketState?, + account: InternalAccountState, + subaccount: InternalSubaccountState?, + ): Map? { + val selectableMarginMode = MarginCalculator.selectableMarginModes( + account = account, + market = market, + subaccountNumber = subaccount?.subaccountNumber ?: 0, + ) + return if (selectableMarginMode) { + mapOf( + "field" to "marginMode", + "type" to "string", + "options" to listOf( + marginModeCross, + marginModeIsolated, + ), + ) + } else { + null + } + } + + private fun sizeField(): Map { + return mapOf( + "field" to "size.size", + "type" to "double", + ) + } + + private fun leverageField(): Map { + return mapOf( + "field" to "size.leverage", + "type" to "double", + ) + } + + private fun limitPriceField(): Map { + return mapOf( + "field" to "price.limitPrice", + "type" to "double", + ) + } + + private fun triggerPriceField(): Map { + return mapOf( + "field" to "price.triggerPrice", + "type" to "double", + ) + } + + private fun trailingPercentField(): Map { + return mapOf("field" to "price.trailingPercent", "type" to "double") + } + + private fun reduceOnlyField(): Map? { + return mapOf( + "field" to "reduceOnly", + "type" to "bool", + "default" to false, + ) + } + + private fun postOnlyField(): Map { + return mapOf( + "field" to "postOnly", + "type" to "bool", + "default" to false, + ) + } + + private fun bracketsField(): Map { + return mapOf( + "field" to "brackets", + "type" to listOf( + stopLossField(), + takeProfitField(), + goodTilField(), + executionField(false), + ), + ) + } + + private fun stopLossField(): Map { + return mapOf( + "field" to "stopLoss", + "type" to + listOf( + priceField(), + reduceOnlyField(), + ).filterNotNull(), + ) + } + + private fun takeProfitField(): Map { + return mapOf( + "field" to "takeProfit", + "type" to + listOf( + priceField(), + reduceOnlyField(), + ).filterNotNull(), + ) + } + + private fun priceField(): Map { + return mapOf( + "field" to "price", + "type" to "double", + ) + } + + private fun timeInForceField(): Map { + return mapOf( + "field" to "timeInForce", + "type" to "string", + "options" to listOf( + timeInForceOptionGTT, + timeInForceOptionIOC, + ), + ) + } + + private fun goodTilField(): Map { + return mapOf( + "field" to "goodTil", + "type" to listOf( + goodTilDurationField(), + goodTilUnitField(), + ), + ) + } + + private fun goodTilDurationField(): Map { + return mapOf( + "field" to "duration", + "type" to "int", + ) + } + + private fun goodTilUnitField(): Map { + return mapOf( + "field" to "unit", + "type" to "string", + "options" to listOf( + goodTilUnitMinutes, + goodTilUnitHours, + goodTilUnitDays, + goodTilUnitWeeks, + ), + ) + } + + private fun executionField(includesDefaultAndPostOnly: Boolean): Map { + return mapOf( + "field" to "execution", + "type" to "string", + "options" to + if (includesDefaultAndPostOnly) { + listOf( + executionDefault, + executionIOC, + executionPostOnly, + ) + } else { + listOf( + executionIOC, + ) + }, + ) + } + + private val timeInForceOptionGTT: SelectionOption + get() = SelectionOption( + type = "GTT", + stringKey = "APP.TRADE.GOOD_TIL_TIME", + string = null, + iconUrl = null, + ) + + private val timeInForceOptionIOC: SelectionOption + get() = SelectionOption( + type = "IOC", + stringKey = "APP.TRADE.IMMEDIATE_OR_CANCEL", + string = null, + iconUrl = null, + ) + + private val goodTilUnitMinutes: SelectionOption + get() = SelectionOption( + type = "M", + stringKey = "APP.GENERAL.TIME_STRINGS.MINUTES_SHORT", + string = null, + iconUrl = null, + ) + + private val goodTilUnitHours: SelectionOption + get() = SelectionOption( + type = "H", + stringKey = "APP.GENERAL.TIME_STRINGS.HOURS", + string = null, + iconUrl = null, + ) + + private val goodTilUnitDays: SelectionOption + get() = SelectionOption( + type = "D", + stringKey = "APP.GENERAL.TIME_STRINGS.DAYS", + string = null, + iconUrl = null, + ) + + private val goodTilUnitWeeks: SelectionOption + get() = SelectionOption( + type = "W", + stringKey = "APP.GENERAL.TIME_STRINGS.WEEKS", + string = null, + iconUrl = null, + ) + + private val executionDefault: SelectionOption + get() = SelectionOption( + type = "DEFAULT", + stringKey = "APP.TRADE.GOOD_TIL_DATE", + string = null, + iconUrl = null, + ) + + private val executionPostOnly: SelectionOption + get() = SelectionOption( + type = "POST_ONLY", + stringKey = "APP.TRADE.POST_ONLY", + string = null, + iconUrl = null, + ) + + private val executionIOC: SelectionOption + get() = SelectionOption( + type = "IOC", + stringKey = "APP.TRADE.IMMEDIATE_OR_CANCEL", + string = null, + iconUrl = null, + ) + + private val marginModeCross: SelectionOption + get() = SelectionOption( + type = "CROSS", + stringKey = "APP.TRADE.CROSS_MARGIN", + string = null, + iconUrl = null, + ) + + private val marginModeIsolated: SelectionOption + get() = SelectionOption( + type = "ISOLATED", + stringKey = "APP.TRADE.ISOLATED_MARGIN", + string = null, + iconUrl = null, + ) +} 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 new file mode 100644 index 000000000..fb281b751 --- /dev/null +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputSummaryCalculator.kt @@ -0,0 +1,481 @@ +package exchange.dydx.abacus.calculator.v2.TradeInput + +import abs +import exchange.dydx.abacus.calculator.CalculationPeriod +import exchange.dydx.abacus.calculator.SlippageConstants.MAJOR_MARKETS +import exchange.dydx.abacus.calculator.SlippageConstants.MARKET_ORDER_MAX_SLIPPAGE +import exchange.dydx.abacus.calculator.SlippageConstants.SLIPPAGE_STEP_SIZE +import exchange.dydx.abacus.calculator.SlippageConstants.STOP_MARKET_ORDER_SLIPPAGE_BUFFER +import exchange.dydx.abacus.calculator.SlippageConstants.STOP_MARKET_ORDER_SLIPPAGE_BUFFER_MAJOR_MARKET +import exchange.dydx.abacus.calculator.SlippageConstants.TAKE_PROFIT_MARKET_ORDER_SLIPPAGE_BUFFER +import exchange.dydx.abacus.calculator.SlippageConstants.TAKE_PROFIT_MARKET_ORDER_SLIPPAGE_BUFFER_MAJOR_MARKET +import exchange.dydx.abacus.output.FeeTier +import exchange.dydx.abacus.output.input.MarginMode +import exchange.dydx.abacus.output.input.OrderSide +import exchange.dydx.abacus.output.input.OrderType +import exchange.dydx.abacus.output.input.TradeInputMarketOrder +import exchange.dydx.abacus.state.internalstate.InternalMarketState +import exchange.dydx.abacus.state.internalstate.InternalRewardsParamsState +import exchange.dydx.abacus.state.internalstate.InternalSubaccountState +import exchange.dydx.abacus.state.internalstate.InternalTradeInputState +import exchange.dydx.abacus.state.internalstate.InternalTradeInputSummary +import exchange.dydx.abacus.state.internalstate.InternalUserState +import exchange.dydx.abacus.utils.Numeric +import exchange.dydx.abacus.utils.QUANTUM_MULTIPLIER +import exchange.dydx.abacus.utils.Rounder +import kotlin.math.abs +import kotlin.math.pow + +internal class TradeInputSummaryCalculator { + fun calculate( + trade: InternalTradeInputState, + subaccount: InternalSubaccountState?, + user: InternalUserState?, + market: InternalMarketState?, + rewardsParams: InternalRewardsParamsState?, + feeTiers: List?, + ): InternalTradeInputState { + trade.summary = when (trade.type) { + OrderType.Market -> { + calculateForMarketOrder(trade, subaccount, user, market, rewardsParams, feeTiers) + } + + OrderType.StopMarket, OrderType.TakeProfitMarket -> { + calculateForStopTakeProfitMarketOrder( + trade, + subaccount, + user, + market, + rewardsParams, + feeTiers, + ) + } + + OrderType.Limit, OrderType.StopLimit, OrderType.TakeProfitLimit -> { + calculateForLimitOrder(trade, subaccount, user, market, rewardsParams, feeTiers) + } + + else -> null + } + + return trade + } + + private fun calculateForMarketOrder( + trade: InternalTradeInputState, + subaccount: InternalSubaccountState?, + user: InternalUserState?, + market: InternalMarketState?, + rewardsParams: InternalRewardsParamsState?, + feeTiers: List?, + ): InternalTradeInputSummary? { + val marketOrder = trade.marketOrder ?: return null + val multiplier = getMultiplier(trade) + + val feeRate = user?.takerFeeRate + val midMarketPrice = marketOrderbookMidPrice(market) + val worstPrice = marketOrder.worstPrice + val slippageFromMidPrice = marketOrderSlippageFromMidPrice(worstPrice, midMarketPrice) + val price = marketOrder.price + val side = trade.side + val payloadPrice = if (price != null) { + when (side) { + OrderSide.Buy -> price * (Numeric.double.ONE + MARKET_ORDER_MAX_SLIPPAGE) + + else -> price * (Numeric.double.ONE - MARKET_ORDER_MAX_SLIPPAGE) + } + } else { + null + } + + val size = marketOrder.size + val usdcSize = + if (price != null && size != null) (price * size) else null + val fee = + if (usdcSize != null && feeRate != null) (usdcSize * feeRate) else null + val total = + if (usdcSize != null) { + usdcSize * multiplier + (fee ?: Numeric.double.ZERO) * Numeric.double.NEGATIVE + } else { + null + } + + val oraclePrice = market?.perpetualMarket?.oraclePrice + val priceDiff = slippage( + price = worstPrice, + oraclePrice = oraclePrice, + side = side, + ) + val indexSlippage = + if (priceDiff != null && oraclePrice != null && oraclePrice > Numeric.double.ZERO) { + Rounder.quickRound( + number = priceDiff / oraclePrice, + stepSize = SLIPPAGE_STEP_SIZE, + ) + } else { + null + } + /* + indexSlippage can be negative. For example, it is OK to buy below index price + */ + val reward = calculateTakerReward( + usdcSize = usdcSize, + fee = fee, + rewardsParams = rewardsParams, + feeTiers = feeTiers, + ) + + return InternalTradeInputSummary( + price = price, + payloadPrice = payloadPrice, + size = size, + usdcSize = usdcSize, + slippage = slippageFromMidPrice, + fee = fee, + total = if (total == Numeric.double.ZERO) Numeric.double.ZERO else total, + reward = reward, + filled = marketOrder.filled, + positionMargin = calculatePositionMargin(trade, subaccount, market), + positionLeverage = getPositionLeverage(subaccount, market), + feeRate = feeRate, + indexSlippage = indexSlippage, + ) + } + + private fun calculateForStopTakeProfitMarketOrder( + trade: InternalTradeInputState, + subaccount: InternalSubaccountState?, + user: InternalUserState?, + market: InternalMarketState?, + rewardsParams: InternalRewardsParamsState?, + feeTiers: List?, + ): InternalTradeInputSummary? { + val marketOrder = trade.marketOrder ?: return null + val multiplier = getMultiplier(trade) + + val feeRate = user?.takerFeeRate + val midMarketPrice = marketOrderbookMidPrice(market) + val worstPrice = marketOrder.worstPrice + val slippageFromMidPrice = marketOrderSlippageFromMidPrice(worstPrice, midMarketPrice) + + val triggerPrice = trade.price?.triggerPrice + val marketOrderPrice = marketOrder.price + val slippagePercentage = + if (midMarketPrice != null && marketOrderPrice != null && midMarketPrice > Numeric.double.ZERO) { + abs((marketOrderPrice - midMarketPrice) / midMarketPrice) + } else { + null + } + + val marketId = trade.marketId + val adjustedslippagePercentage = if (slippagePercentage != null) { + val majorMarket = MAJOR_MARKETS.contains(marketId) + if (majorMarket) { + if (trade.type == OrderType.StopMarket) { + slippagePercentage + STOP_MARKET_ORDER_SLIPPAGE_BUFFER_MAJOR_MARKET + } else { + slippagePercentage + TAKE_PROFIT_MARKET_ORDER_SLIPPAGE_BUFFER_MAJOR_MARKET + } + } else { + if (trade.type == OrderType.StopMarket) { + slippagePercentage + STOP_MARKET_ORDER_SLIPPAGE_BUFFER + } else { + slippagePercentage + TAKE_PROFIT_MARKET_ORDER_SLIPPAGE_BUFFER + } + } + } else { + null + } + + val price = if (triggerPrice != null && slippageFromMidPrice != null) { + if (trade.side == OrderSide.Buy) { + triggerPrice * (Numeric.double.ONE + slippageFromMidPrice) + } else { + triggerPrice * (Numeric.double.ONE - slippageFromMidPrice) + } + } else { + null + } + + val payloadPrice = + if (triggerPrice != null && adjustedslippagePercentage != null) { + if (trade.side == OrderSide.Buy) { + triggerPrice * (Numeric.double.ONE + adjustedslippagePercentage) + } else { + triggerPrice * (Numeric.double.ONE - adjustedslippagePercentage) + } + } else { + null + } + + val size = marketOrder.size + val usdcSize = + if (price != null && size != null) (price * size) else null + val fee = + if (usdcSize != null && feeRate != null) (usdcSize * feeRate) else null + val total = + if (usdcSize != null) { + usdcSize * multiplier + (fee ?: Numeric.double.ZERO) * Numeric.double.NEGATIVE + } else { + null + } + + val reward = calculateTakerReward( + usdcSize = usdcSize, + fee = fee, + rewardsParams = rewardsParams, + feeTiers = feeTiers, + ) + + return InternalTradeInputSummary( + price = price, + payloadPrice = payloadPrice, + size = size, + usdcSize = usdcSize, + slippage = slippageFromMidPrice, + fee = fee, + total = if (total == Numeric.double.ZERO) Numeric.double.ZERO else total, + reward = reward, + filled = marketOrder.filled, + positionMargin = calculatePositionMargin(trade, subaccount, market), + positionLeverage = getPositionLeverage(subaccount, market), + feeRate = feeRate, + indexSlippage = null, + ) + } + + private fun calculateForLimitOrder( + trade: InternalTradeInputState, + subaccount: InternalSubaccountState?, + user: InternalUserState?, + market: InternalMarketState?, + rewardsParams: InternalRewardsParamsState?, + feeTiers: List?, + ): InternalTradeInputSummary { + val multiplier = getMultiplier(trade) + + val timeInForce = trade.timeInForce + val execution = trade.execution + val isMaker = + (trade.type == OrderType.Limit && timeInForce == "GTT") || execution == "POST_ONLY" + + val feeRate = if (isMaker) user?.makerFeeRate else user?.takerFeeRate + + val price = trade.price?.limitPrice + val size = trade.size?.size + val usdcSize = + if (price != null && size != null) (price * size) else null + val fee = + if (usdcSize != null && feeRate != null) (usdcSize * feeRate) else null + val total = + if (usdcSize != null) { + usdcSize * multiplier + (fee ?: Numeric.double.ZERO) * Numeric.double.NEGATIVE + } else { + null + } + + val reward = + if (isMaker) { + calculateMakerReward( + fee = fee, + rewardsParams = rewardsParams, + ) + } else { + calculateTakerReward( + usdcSize = usdcSize, + fee = fee, + rewardsParams = rewardsParams, + feeTiers = feeTiers, + ) + } + + return InternalTradeInputSummary( + price = price, + payloadPrice = price, + size = size, + usdcSize = usdcSize, + slippage = null, + fee = fee, + total = if (total == Numeric.double.ZERO) Numeric.double.ZERO else total, + reward = reward, + filled = true, + positionMargin = calculatePositionMargin(trade, subaccount, market), + positionLeverage = getPositionLeverage(subaccount, market), + feeRate = feeRate, + indexSlippage = null, + ) + } + + private fun marketOrderbookMidPrice(market: InternalMarketState?): Double? { + val orderbook = market?.consolidatedOrderbook + val firstAskPrice = orderbook?.asks?.firstOrNull()?.price + val firstBidPrice = orderbook?.bids?.firstOrNull()?.price + return if (firstAskPrice != null && firstBidPrice != null) { + (firstAskPrice + firstBidPrice) / 2.0 + } else { + null + } + } + + private fun marketOrderSlippageFromMidPrice( + worstPrice: Double?, + midMarketPrice: Double? + ): Double? { + return if (worstPrice != null && midMarketPrice != null && midMarketPrice > Numeric.double.ZERO) { + Rounder.round( + number = (worstPrice - midMarketPrice).abs() / midMarketPrice, + stepSize = SLIPPAGE_STEP_SIZE, + ) + } else { + null + } + } + + private fun marketOrderWorstPrice(marketOrder: TradeInputMarketOrder): Double? { + return marketOrder.worstPrice + } + + private fun slippage(price: Double?, oraclePrice: Double?, side: OrderSide?): Double? { + return if (price != null && oraclePrice != null) { + if (side == OrderSide.Buy) price - oraclePrice else oraclePrice - price + } else { + null + } + } + + private fun getMultiplier(trade: InternalTradeInputState): Double { + return if (trade.side == OrderSide.Buy) Numeric.double.POSITIVE else Numeric.double.NEGATIVE + } + + private fun calculateTakerReward( + usdcSize: Double?, + fee: Double?, + rewardsParams: InternalRewardsParamsState?, + feeTiers: List?, + ): Double? { + val feeMultiplierPpm = rewardsParams?.feeMultiplierPpm + val tokenPrice = rewardsParams?.tokenPrice + val tokenPriceExponent = rewardsParams?.tokenExpoonent + val notional = usdcSize + val maxMakerRebate = findMaxMakerRebate(feeTiers) + + if (fee != null && + feeMultiplierPpm != null && + tokenPrice != null && + tokenPriceExponent != null && + fee > 0.0 && + notional != null && + tokenPrice > 0.0 + ) { + val feeMultiplier = feeMultiplierPpm / QUANTUM_MULTIPLIER + return feeMultiplier * (fee - maxMakerRebate * notional) / ( + tokenPrice * 10.0.pow(tokenPriceExponent) + ) + } + return null + } + + private fun calculateMakerReward( + fee: Double?, + rewardsParams: InternalRewardsParamsState? + ): Double? { + val feeMultiplierPpm = rewardsParams?.feeMultiplierPpm + val tokenPrice = rewardsParams?.tokenPrice + val tokenPriceExponent = rewardsParams?.tokenExpoonent + + if (fee != null && + feeMultiplierPpm != null && + tokenPrice != null && + tokenPriceExponent != null && + fee > 0.0 && + tokenPrice > 0.0 + ) { + val feeMultiplier = feeMultiplierPpm / QUANTUM_MULTIPLIER + return fee * feeMultiplier / (tokenPrice * 10.0.pow(tokenPriceExponent)) + } + return null + } + + private fun findMaxMakerRebate(feeTiers: List?): Double { + if (feeTiers.isNullOrEmpty()) return 0.0 + + val smallestNegative = feeTiers.map { it.maker ?: 0.0 } + .filter { it < 0.0 } + .minOrNull() + + return abs(smallestNegative ?: 0.0) + } + + /** + * Calculate the current and postOrder position margin to be displayed in the TradeInput Summary. + */ + private fun calculatePositionMargin( + trade: InternalTradeInputState, + subaccount: InternalSubaccountState?, + market: InternalMarketState?, + ): Double? { + if (subaccount == null || market == null) { + return null + } + + val marginMode = trade.marginMode + val marketId = market.perpetualMarket?.id ?: return null + val position = subaccount.openPositions?.get(marketId) + + if (position != null) { + when (marginMode) { + MarginMode.Isolated -> { + val currentEquity = subaccount.calculated[CalculationPeriod.current]?.equity + val postOrderEquity = subaccount.calculated[CalculationPeriod.post]?.equity + if (currentEquity != null) { + if (postOrderEquity != null) { + return postOrderEquity + } + return currentEquity + } + } + + MarginMode.Cross -> { + val currentNotionalTotal = + position.calculated[CalculationPeriod.current]?.notionalTotal + val postOrderNotionalTotal = + position.calculated[CalculationPeriod.post]?.notionalTotal + val mmf = market.perpetualMarket?.configs?.maintenanceMarginFraction + if (currentNotionalTotal != null && mmf != null) { + if (postOrderNotionalTotal != null) { + return postOrderNotionalTotal.times(mmf) + } + return currentNotionalTotal.times(mmf) + } + } + + else -> return null + } + } + return null + } + + /** + * Return Subaccount leverage to display as Position leverage in the TradeInput Summary. + * Use Subaccount leverage since a subaccount can only have 1 isolated position + */ + private fun getPositionLeverage( + subaccount: InternalSubaccountState?, + market: InternalMarketState?, + ): Double? { + if (subaccount == null || market == null) return null + + if (!subaccount.isParentSubaccount) { + val currentLeverage = subaccount.calculated[CalculationPeriod.current]?.leverage + val postOrderLeverage = subaccount.calculated[CalculationPeriod.post]?.leverage + return postOrderLeverage ?: currentLeverage + } + + val marketId = market.perpetualMarket?.id + val position = subaccount.openPositions?.get(marketId) ?: return null + + val currentLeverage = position.calculated[CalculationPeriod.current]?.leverage + val postOrderLeverage = position.calculated[CalculationPeriod.post]?.leverage + return postOrderLeverage ?: currentLeverage + } +} 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 e4dbab884..055c1ea90 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/output/input/TradeInput.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/TradeInput.kt @@ -191,7 +191,7 @@ data class TradeInputOptions( typeOptions = typeOptionsV4Array, sideOptions = sideOptionsArray, timeInForceOptions = state.timeInForceOptions?.toIList(), - goodTilUnitOptions = goodTilUnitOptionsArray, + goodTilUnitOptions = state.goodTilUnitOptions?.toIList() ?: goodTilUnitOptionsArray, executionOptions = state.executionOptions?.toIList(), marginModeOptions = state.marginModeOptions?.toIList(), reduceOnlyTooltip = state.reduceOnlyTooltip, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt index 98f83eb9b..592186aea 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt @@ -78,11 +78,28 @@ internal data class InternalTradeInputState( var bracket: TradeInputBracket? = null, var options: InternalTradeInputOptions = InternalTradeInputOptions(), var marketOrder: TradeInputMarketOrder? = null, + var summary: InternalTradeInputSummary? = null, ) { val isBuying: Boolean get() = side == OrderSide.Buy || side == null } +internal data class InternalTradeInputSummary( + val price: Double?, + val payloadPrice: Double?, + val size: Double?, + val usdcSize: Double?, + val slippage: Double?, + val fee: Double?, + val total: Double?, + val reward: Double?, + val filled: Boolean, + val positionMargin: Double?, + val positionLeverage: Double?, + val indexSlippage: Double? = null, + val feeRate: Double? = null, +) + internal data class InternalTradeInputOptions( var needsMarginMode: Boolean = false, var needsSize: Boolean = false, @@ -99,6 +116,7 @@ internal data class InternalTradeInputOptions( var timeInForceOptions: List? = null, var executionOptions: List? = null, var marginModeOptions: List? = null, + var goodTilUnitOptions: List? = null, var reduceOnlyTooltip: Tooltip? = null, var postOnlyTooltip: Tooltip? = null, ) diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/payload/BaseTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/payload/BaseTests.kt index 8ce4ebcb2..3a3321b78 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/payload/BaseTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/payload/BaseTests.kt @@ -87,7 +87,7 @@ internal typealias VerificationFunction = (response: StateResponse) -> Unit open class BaseTests( private val maxSubaccountNumber: Int, private val useParentSubaccount: Boolean, - private val staticTyping: Boolean = true, // turn on static typing for testing + private val staticTyping: Boolean = false, // turn on static typing for testing ) { open val doAsserts = true internal val deploymentUri = "https://api.examples.com" From 3c81c65e0fb0733aa669433a9f09c534c939bf66 Mon Sep 17 00:00:00 2001 From: Rui Date: Mon, 12 Aug 2024 21:02:03 -0700 Subject: [PATCH 28/63] Working more or less --- .../output/input/TradeInput.kt | 24 +++- .../input/TradeInputField+Actions.kt | 30 +++- .../processor/input/TradeInputProcessor.kt | 130 +++++++++--------- .../state/internalstate/InternalState.kt | 7 +- .../model/TradingStateMachine+TradeInput.kt | 11 +- .../state/model/TradingStateMachine.kt | 104 +++++++++----- 6 files changed, 193 insertions(+), 113 deletions(-) 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 055c1ea90..49ddbf8e6 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/output/input/TradeInput.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/TradeInput.kt @@ -1,8 +1,10 @@ package exchange.dydx.abacus.output.input import exchange.dydx.abacus.protocols.ParserProtocol +import exchange.dydx.abacus.state.internalstate.InternalMarketSummaryState import exchange.dydx.abacus.state.internalstate.InternalTradeInputOptions import exchange.dydx.abacus.state.internalstate.InternalTradeInputState +import exchange.dydx.abacus.state.internalstate.InternalTradeInputSummary import exchange.dydx.abacus.utils.IList import exchange.dydx.abacus.utils.IMutableList import exchange.dydx.abacus.utils.Logger @@ -349,6 +351,24 @@ data class TradeInputSummary( val positionLeverage: Double? ) { companion object { + internal fun create( + state: InternalTradeInputSummary?, + ): TradeInputSummary? { + return TradeInputSummary( + price = state?.price, + payloadPrice = state?.payloadPrice, + size = state?.size, + usdcSize = state?.usdcSize, + slippage = state?.slippage, + fee = state?.fee, + total = state?.total, + reward = state?.reward, + filled = state?.filled ?: false, + positionMargin = state?.positionMargin, + positionLeverage = state?.positionLeverage, + ) + } + internal fun create( existing: TradeInputSummary?, parser: ParserProtocol, @@ -816,9 +836,9 @@ data class TradeInput( marginMode = state.marginMode ?: MarginMode.Cross, targetLeverage = state.targetLeverage ?: 1.0, bracket = state.bracket, - marketOrder = null, // TODO + marketOrder = state.marketOrder, options = TradeInputOptions.create(state.options), - summary = null, // TODO + summary = TradeInputSummary.create(state.summary), ) } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputField+Actions.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputField+Actions.kt index ca1865125..4eaabb8a8 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputField+Actions.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputField+Actions.kt @@ -109,8 +109,13 @@ internal val TradeInputField.valueAction: ((InternalTradeInputState) -> Any?)? // Returns the write action to update value for the trade input field internal val TradeInputField.updateValueAction: ((InternalTradeInputState, String?, ParserProtocol) -> Unit)? get() = when (this) { - type -> { trade, value, parser -> trade.type = OrderType.invoke(value) } - side -> { trade, value, parser -> trade.side = OrderSide.invoke(value) } + type -> { trade, value, parser -> + trade.type = OrderType.invoke(value) + } + + side -> { trade, value, parser -> + trade.side = OrderSide.invoke(value) + } TradeInputField.lastInput -> { trade, value, parser -> trade.size = TradeInputSize.safeCreate(trade.size).copy(input = value) @@ -162,7 +167,9 @@ internal val TradeInputField.updateValueAction: ((InternalTradeInputState, Strin trade.marginMode = MarginMode.invoke(value) } - timeInForceType -> { trade, value, parser -> trade.timeInForce = value } + timeInForceType -> { trade, value, parser -> + trade.timeInForce = value + } goodTilUnit -> { trade, value, parser -> trade.goodTil = TradeInputGoodUntil.safeCreate(trade.goodTil).copy(unit = value) @@ -175,7 +182,9 @@ internal val TradeInputField.updateValueAction: ((InternalTradeInputState, Strin ) } - execution -> { trade, value, parser -> trade.execution = value } + execution -> { trade, value, parser -> + trade.execution = value + } bracketsExecution -> { trade, value, parser -> trade.bracket = TradeInputBracket.safeCreate(trade.bracket).copy(execution = value) @@ -194,9 +203,13 @@ internal val TradeInputField.updateValueAction: ((InternalTradeInputState, Strin ) } - reduceOnly -> { trade, value, parser -> trade.reduceOnly = parser.asBool(value) ?: false } + reduceOnly -> { trade, value, parser -> + trade.reduceOnly = parser.asBool(value) ?: false + } - postOnly -> { trade, value, parser -> trade.postOnly = parser.asBool(value) ?: false } + postOnly -> { trade, value, parser -> + trade.postOnly = parser.asBool(value) ?: false + } bracketsStopLossReduceOnly -> { trade, value, parser -> val braket = TradeInputBracket.safeCreate(trade.bracket) @@ -215,7 +228,10 @@ internal val TradeInputField.updateValueAction: ((InternalTradeInputState, Strin ) } - targetLeverage -> { trade, value, parser -> trade.targetLeverage = parser.asDouble(value) } + targetLeverage -> { trade, value, parser -> + trade.targetLeverage = parser.asDouble(value) + } + size -> { trade, value, parser -> trade.size = TradeInputSize.safeCreate(trade.size).copy(size = parser.asDouble(value)) } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputProcessor.kt index 089010650..daffa329f 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputProcessor.kt @@ -5,6 +5,7 @@ import exchange.dydx.abacus.calculator.CalculationPeriod import exchange.dydx.abacus.calculator.MarginCalculator import exchange.dydx.abacus.calculator.TradeCalculation import exchange.dydx.abacus.calculator.TradeInputCalculator +import exchange.dydx.abacus.calculator.v2.TradeInput.TradeInputCalculatorV2 import exchange.dydx.abacus.output.PerpetualMarketType import exchange.dydx.abacus.output.input.MarginMode import exchange.dydx.abacus.output.input.OrderSide @@ -17,11 +18,14 @@ import exchange.dydx.abacus.responses.cannotModify import exchange.dydx.abacus.state.changes.Changes import exchange.dydx.abacus.state.changes.StateChanges import exchange.dydx.abacus.state.internalstate.InternalAccountState +import exchange.dydx.abacus.state.internalstate.InternalConfigsState import exchange.dydx.abacus.state.internalstate.InternalInputState import exchange.dydx.abacus.state.internalstate.InternalInputType import exchange.dydx.abacus.state.internalstate.InternalMarketState +import exchange.dydx.abacus.state.internalstate.InternalMarketSummaryState import exchange.dydx.abacus.state.internalstate.InternalTradeInputOptions 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.model.TradeInputField import exchange.dydx.abacus.state.model.TradeInputField.bracketsExecution @@ -55,11 +59,22 @@ import kotlin.math.abs internal interface TradeInputProcessorProtocol { fun tradeInMarket( inputState: InternalInputState, - marketState: InternalMarketState, - accountState: InternalAccountState, + marketSummaryState: InternalMarketSummaryState, + walletState: InternalWalletState, + configs: InternalConfigsState, marketId: String, subaccountNumber: Int, ): StateChanges + + fun trade( + inputState: InternalInputState, + walletState: InternalWalletState, + marketSummaryState: InternalMarketSummaryState, + configs: InternalConfigsState, + inputData: String?, + inputType: TradeInputField?, + subaccountNumber: Int, + ): TradeInputResult } internal class TradeInputResult( @@ -69,20 +84,21 @@ internal class TradeInputResult( internal class TradeInputProcessor( private val parser: ParserProtocol, - private val calculator: TradeInputCalculator = TradeInputCalculator(parser, TradeCalculation.trade) + private val calculator: TradeInputCalculatorV2 = TradeInputCalculatorV2(parser, TradeCalculation.trade) ) : TradeInputProcessorProtocol { override fun tradeInMarket( inputState: InternalInputState, - marketState: InternalMarketState, - accountState: InternalAccountState, + marketSummaryState: InternalMarketSummaryState, + walletState: InternalWalletState, + configs: InternalConfigsState, marketId: String, subaccountNumber: Int, ): StateChanges { if (inputState.trade.marketId == marketId) { - if (inputState.currentType == InternalInputType.TRADE) { + if (inputState.currentType == InternalInputType.Trade) { return StateChanges(iListOf()) // no change } else { - inputState.currentType = InternalInputType.TRADE + inputState.currentType = InternalInputType.Trade return StateChanges( changes = iListOf(Changes.input), markets = null, @@ -102,25 +118,27 @@ internal class TradeInputProcessor( inputState.trade = initialTradeInputState( marketId = marketId, subaccountNumber = subaccountNumber, - accountState = accountState, - marketState = marketState, + walletState = walletState, + marketSummaryState = marketSummaryState, + configs = configs, ) } + val market = marketSummaryState.markets[marketId] initiateMarginModeLeverage( trade = inputState.trade, - marketState = marketState, - accountState = accountState, + marketState = market, + accountState = walletState.account, marketId = marketId, subaccountNumber = subaccountNumber, ) - inputState.currentType = InternalInputType.TRADE + inputState.currentType = InternalInputType.Trade val subaccountNumbers = MarginCalculator.getChangedSubaccountNumbers( parser = parser, - subaccounts = accountState.subaccounts, + subaccounts = walletState.account.subaccounts, subaccountNumber = subaccountNumber, tradeInput = inputState.trade, ) @@ -131,30 +149,33 @@ internal class TradeInputProcessor( ) } - fun trade( + override fun trade( inputState: InternalInputState, - accountState: InternalAccountState, + walletState: InternalWalletState, + marketSummaryState: InternalMarketSummaryState, + configs: InternalConfigsState, inputData: String?, inputType: TradeInputField?, subaccountNumber: Int, ): TradeInputResult { - inputState.currentType = InternalInputType.TRADE + inputState.currentType = InternalInputType.Trade if (inputState.trade.marketId == null) { // new trade inputState.trade = initialTradeInputState( marketId = null, subaccountNumber = subaccountNumber, - accountState = accountState, - marketState = null, + walletState = walletState, + marketSummaryState = marketSummaryState, + configs = configs, ) } if (inputType == null) { return TradeInputResult( changes = StateChanges( - iListOf(Changes.wallet, Changes.subaccount, Changes.input), - null, - iListOf(subaccountNumber), + changes = iListOf(Changes.wallet, Changes.subaccount, Changes.input), + markets = null, + subaccountNumbers = iListOf(subaccountNumber), ), ) } @@ -169,7 +190,7 @@ internal class TradeInputProcessor( val subaccountNumbers = MarginCalculator.getChangedSubaccountNumbers( parser = parser, - subaccounts = accountState.subaccounts, + subaccounts = walletState.account.subaccounts, subaccountNumber = subaccountNumber, tradeInput = trade, ) @@ -242,7 +263,7 @@ internal class TradeInputProcessor( val changedSubaccountNumbers = MarginCalculator.getChangedSubaccountNumbers( parser = parser, - subaccounts = accountState.subaccounts, + subaccounts = walletState.account.subaccounts, subaccountNumber = subaccountNumber, tradeInput = trade, ) @@ -277,7 +298,7 @@ internal class TradeInputProcessor( private fun initiateMarginModeLeverage( trade: InternalTradeInputState, - marketState: InternalMarketState, + marketState: InternalMarketState?, accountState: InternalAccountState, marketId: String, subaccountNumber: Int, @@ -306,7 +327,7 @@ internal class TradeInputProcessor( if (existingOrder.subaccountNumber == subaccountNumber) MarginMode.Cross else MarginMode.Isolated trade.targetLeverage = 1.0 } else { - val marketType = marketState.perpetualMarket?.configs?.perpetualMarketType + val marketType = marketState?.perpetualMarket?.configs?.perpetualMarketType trade.marginMode = when (marketType) { PerpetualMarketType.CROSS -> MarginMode.Cross PerpetualMarketType.ISOLATED -> MarginMode.Isolated @@ -319,51 +340,34 @@ internal class TradeInputProcessor( private fun initialTradeInputState( marketId: String?, subaccountNumber: Int, - accountState: InternalAccountState, - marketState: InternalMarketState?, + walletState: InternalWalletState, + marketSummaryState: InternalMarketSummaryState, + configs: InternalConfigsState, ): InternalTradeInputState { -// -// val trade = exchange.dydx.abacus.utils.mutableMapOf() -// trade["type"] = "LIMIT" -// trade["side"] = "BUY" -// trade["marketId"] = marketId ?: "ETH-USD" - -// val marginMode = MarginCalculator.findExistingMarginModeDeprecated(parser, account, marketId, subaccountNumber) -// ?: MarginCalculator.findMarketMarginMode(parser, parser.asNativeMap(parser.value(marketsSummary, "markets.$marketId"))) -// -// trade.safeSet("marginMode", marginMode) -// -// val calculator = TradeInputCalculator(parser, TradeCalculation.trade) -// val params = exchange.dydx.abacus.utils.mutableMapOf() -// params.safeSet("markets", parser.asMap(marketsSummary?.get("markets"))) -// params.safeSet("account", account) -// params.safeSet("user", user) -// params.safeSet("trade", trade) -// params.safeSet("rewardsParams", rewardsParams) -// params.safeSet("configs", configs) -// -// val modified = calculator.calculate(params, subaccountNumber, null) -// -// return parser.asMap(modified["trade"])?.mutable() ?: trade - + val market = marketSummaryState.markets[marketId] val marginMode = MarginCalculator.findExistingMarginMode( - account = accountState, + account = walletState.account, marketId = marketId, subaccountNumber = subaccountNumber, ) ?: MarginCalculator.findMarketMarginMode( - market = marketState?.perpetualMarket, + market = market?.perpetualMarket, ) - // TODO - implement TradeInputCalculatorV2 - // calculator.calculate() - - return InternalTradeInputState( - marketId = marketId, - size = null, - price = null, - type = OrderType.Limit, - side = OrderSide.Buy, - marginMode = marginMode, + return calculator.calculate( + trade = InternalTradeInputState( + marketId = marketId ?: "ETH-USD", + size = null, + price = null, + type = OrderType.Limit, + side = OrderSide.Buy, + marginMode = marginMode, + ), + wallet = walletState, + marketSummary = marketSummaryState, + rewardsParams = null, + configs = configs, + subaccountNumber = subaccountNumber, + input = null, ) } } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt index 592186aea..c6c891d6b 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt @@ -51,8 +51,11 @@ internal data class InternalState( ) internal enum class InternalInputType { - TRADE, - TRANSFER, + Trade, + Transfer, + TriggerOrder, + AdjustIsolatedMargin, + ClosePosition, } internal data class InternalInputState( diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt index a1d2f5751..c60eb1dd6 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt @@ -90,12 +90,11 @@ internal fun TradingStateMachine.tradeInMarket( subaccountNumber: Int, ): StateResponse { if (staticTyping) { - val market = internalState.marketsSummary.markets[marketId] - ?: return StateResponse(state, StateChanges(iListOf()), null) val changes = tradeInputProcessor.tradeInMarket( inputState = internalState.input, - marketState = market, - accountState = internalState.wallet.account, + marketSummaryState = internalState.marketsSummary, + walletState = internalState.wallet, + configs = internalState.configs, marketId = marketId, subaccountNumber = subaccountNumber, ) @@ -226,7 +225,9 @@ fun TradingStateMachine.trade( if (staticTyping) { val result = tradeInputProcessor.trade( inputState = internalState.input, - accountState = internalState.wallet.account, + walletState = internalState.wallet, + marketSummaryState = internalState.marketsSummary, + configs = internalState.configs, inputType = type, inputData = data, subaccountNumber = subaccountNumber, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt index f9e6d4f1f..787555c37 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt @@ -9,6 +9,7 @@ import exchange.dydx.abacus.calculator.TradeInputCalculator import exchange.dydx.abacus.calculator.TransferInputCalculator import exchange.dydx.abacus.calculator.TriggerOrdersInputCalculator import exchange.dydx.abacus.calculator.v2.AccountCalculatorV2 +import exchange.dydx.abacus.calculator.v2.TradeInput.TradeInputCalculatorV2 import exchange.dydx.abacus.output.Asset import exchange.dydx.abacus.output.Configs import exchange.dydx.abacus.output.LaunchIncentive @@ -54,6 +55,7 @@ import exchange.dydx.abacus.state.app.helper.Formatter import exchange.dydx.abacus.state.changes.Changes import exchange.dydx.abacus.state.changes.StateChanges import exchange.dydx.abacus.state.internalstate.InternalAccountState +import exchange.dydx.abacus.state.internalstate.InternalInputType import exchange.dydx.abacus.state.internalstate.InternalState import exchange.dydx.abacus.state.manager.BlockAndTime import exchange.dydx.abacus.state.manager.EnvironmentFeatureFlags @@ -646,28 +648,49 @@ open class TradingStateMachine( ) if (subaccountNumber != null) { - when (this.input?.get("current")) { - "trade" -> { - calculateTrade(subaccountNumber) + if (staticTyping) { + when (internalState.input.currentType) { + InternalInputType.Trade -> { + calculateTrade(subaccountNumber) + } + InternalInputType.Transfer -> { + calculateTransfer(subaccountNumber) + } + InternalInputType.TriggerOrder -> { + calculateTriggerOrders(subaccountNumber) + } + InternalInputType.AdjustIsolatedMargin -> { + calculateAdjustIsolatedMargin(subaccountNumber) + } + InternalInputType.ClosePosition -> { + calculateClosePosition(subaccountNumber) + } + else -> {} } + } else { + when (this.input?.get("current")) { + "trade" -> { + calculateTrade(subaccountNumber) + } - "closePosition" -> { - calculateClosePosition(subaccountNumber) - } + "closePosition" -> { + calculateClosePosition(subaccountNumber) + } - "transfer" -> { - calculateTransfer(subaccountNumber) - } + "transfer" -> { + calculateTransfer(subaccountNumber) + } - "triggerOrders" -> { - calculateTriggerOrders(subaccountNumber) - } + "triggerOrders" -> { + calculateTriggerOrders(subaccountNumber) + } - "adjustIsolatedMargin" -> { - calculateAdjustIsolatedMargin(subaccountNumber) - } + "adjustIsolatedMargin" -> { + calculateAdjustIsolatedMargin(subaccountNumber) + } - else -> {} + else -> {} + } } } } @@ -726,24 +749,37 @@ open class TradingStateMachine( } private fun calculateTrade(tag: String, calculation: TradeCalculation, subaccountNumber: Int) { - val input = this.input?.mutable() - val trade = parser.asNativeMap(input?.get(tag)) - val inputType = parser.asString(parser.value(trade, "size.input")) - val calculator = TradeInputCalculator(parser, calculation) - val params = mutableMapOf() - params.safeSet("markets", parser.asNativeMap(marketsSummary?.get("markets"))) - params.safeSet("account", account) - params.safeSet("user", user) - params.safeSet("trade", trade) - params.safeSet("rewardsParams", rewardsParams) - params.safeSet("configs", configs) - - val modified = calculator.calculate(params, subaccountNumber, inputType) - this.setMarkets(parser.asNativeMap(modified["markets"])) - this.account = parser.asNativeMap(modified["account"]) - input?.safeSet(tag, parser.asNativeMap(modified["trade"])) - - this.input = input + if (staticTyping) { + val calculator = TradeInputCalculatorV2(parser, calculation) + calculator.calculate( + trade = internalState.input.trade, + wallet = internalState.wallet, + marketSummary = internalState.marketsSummary, + rewardsParams = internalState.rewardsParams, + configs = internalState.configs, + subaccountNumber = subaccountNumber, + input = internalState.input.trade.size?.input, + ) + } else { + val input = this.input?.mutable() + val trade = parser.asNativeMap(input?.get(tag)) + val inputType = parser.asString(parser.value(trade, "size.input")) + val calculator = TradeInputCalculator(parser, calculation) + val params = mutableMapOf() + params.safeSet("markets", parser.asNativeMap(marketsSummary?.get("markets"))) + params.safeSet("account", account) + params.safeSet("user", user) + params.safeSet("trade", trade) + params.safeSet("rewardsParams", rewardsParams) + params.safeSet("configs", configs) + + val modified = calculator.calculate(params, subaccountNumber, inputType) + this.setMarkets(parser.asNativeMap(modified["markets"])) + this.account = parser.asNativeMap(modified["account"]) + input?.safeSet(tag, parser.asNativeMap(modified["trade"])) + + this.input = input + } } private fun calculateClosePosition(subaccountNumber: Int) { From 1fd42dfa10299403f5adb2bbcd119d970bb12a5e Mon Sep 17 00:00:00 2001 From: Rui Date: Tue, 13 Aug 2024 09:57:52 -0700 Subject: [PATCH 29/63] Updating tests --- .../output/input/Input.kt | 2 +- .../processor/input/TradeInputProcessor.kt | 10 +- .../state/internalstate/InternalState.kt | 11 +- .../state/model/TradingStateMachine.kt | 12 +- .../payload/TradeInputOptionsTests.kt | 209 ++++++++++++++---- .../payload/v4/V4NoAccountTradeInputTests.kt | 18 +- 6 files changed, 197 insertions(+), 65 deletions(-) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/output/input/Input.kt b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/Input.kt index 7bd4d7656..da67e534e 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/output/input/Input.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/Input.kt @@ -19,7 +19,7 @@ enum class InputType(val rawValue: String) { companion object { operator fun invoke(rawValue: String?) = - InputType.values().firstOrNull { it.rawValue == rawValue } + entries.firstOrNull { it.rawValue == rawValue } } } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputProcessor.kt index daffa329f..f520e9982 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputProcessor.kt @@ -7,6 +7,7 @@ import exchange.dydx.abacus.calculator.TradeCalculation import exchange.dydx.abacus.calculator.TradeInputCalculator import exchange.dydx.abacus.calculator.v2.TradeInput.TradeInputCalculatorV2 import exchange.dydx.abacus.output.PerpetualMarketType +import exchange.dydx.abacus.output.input.InputType import exchange.dydx.abacus.output.input.MarginMode import exchange.dydx.abacus.output.input.OrderSide import exchange.dydx.abacus.output.input.OrderType @@ -20,7 +21,6 @@ import exchange.dydx.abacus.state.changes.StateChanges import exchange.dydx.abacus.state.internalstate.InternalAccountState import exchange.dydx.abacus.state.internalstate.InternalConfigsState import exchange.dydx.abacus.state.internalstate.InternalInputState -import exchange.dydx.abacus.state.internalstate.InternalInputType import exchange.dydx.abacus.state.internalstate.InternalMarketState import exchange.dydx.abacus.state.internalstate.InternalMarketSummaryState import exchange.dydx.abacus.state.internalstate.InternalTradeInputOptions @@ -95,10 +95,10 @@ internal class TradeInputProcessor( subaccountNumber: Int, ): StateChanges { if (inputState.trade.marketId == marketId) { - if (inputState.currentType == InternalInputType.Trade) { + if (inputState.currentType == InputType.TRADE) { return StateChanges(iListOf()) // no change } else { - inputState.currentType = InternalInputType.Trade + inputState.currentType = InputType.TRADE return StateChanges( changes = iListOf(Changes.input), markets = null, @@ -133,7 +133,7 @@ internal class TradeInputProcessor( subaccountNumber = subaccountNumber, ) - inputState.currentType = InternalInputType.Trade + inputState.currentType = InputType.TRADE val subaccountNumbers = MarginCalculator.getChangedSubaccountNumbers( @@ -158,7 +158,7 @@ internal class TradeInputProcessor( inputType: TradeInputField?, subaccountNumber: Int, ): TradeInputResult { - inputState.currentType = InternalInputType.Trade + inputState.currentType = InputType.TRADE if (inputState.trade.marketId == null) { // new trade diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt index c6c891d6b..3da762951 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt @@ -20,6 +20,7 @@ import exchange.dydx.abacus.output.account.SubaccountOrder import exchange.dydx.abacus.output.account.SubaccountPositionResources import exchange.dydx.abacus.output.account.SubaccountTransfer import exchange.dydx.abacus.output.account.UnbondingDelegation +import exchange.dydx.abacus.output.input.InputType import exchange.dydx.abacus.output.input.MarginMode import exchange.dydx.abacus.output.input.OrderSide import exchange.dydx.abacus.output.input.OrderType @@ -50,17 +51,9 @@ internal data class InternalState( val input: InternalInputState = InternalInputState(), ) -internal enum class InternalInputType { - Trade, - Transfer, - TriggerOrder, - AdjustIsolatedMargin, - ClosePosition, -} - internal data class InternalInputState( var trade: InternalTradeInputState = InternalTradeInputState(), - var currentType: InternalInputType? = null, + var currentType: InputType? = null, ) internal data class InternalTradeInputState( diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt index 787555c37..c24d087bd 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt @@ -31,6 +31,7 @@ import exchange.dydx.abacus.output.account.SubaccountFundingPayment import exchange.dydx.abacus.output.account.SubaccountHistoricalPNL import exchange.dydx.abacus.output.account.SubaccountTransfer import exchange.dydx.abacus.output.input.Input +import exchange.dydx.abacus.output.input.InputType import exchange.dydx.abacus.output.input.ReceiptLine import exchange.dydx.abacus.processor.assets.AssetsProcessor import exchange.dydx.abacus.processor.configs.ConfigsProcessor @@ -55,7 +56,6 @@ import exchange.dydx.abacus.state.app.helper.Formatter import exchange.dydx.abacus.state.changes.Changes import exchange.dydx.abacus.state.changes.StateChanges import exchange.dydx.abacus.state.internalstate.InternalAccountState -import exchange.dydx.abacus.state.internalstate.InternalInputType import exchange.dydx.abacus.state.internalstate.InternalState import exchange.dydx.abacus.state.manager.BlockAndTime import exchange.dydx.abacus.state.manager.EnvironmentFeatureFlags @@ -650,19 +650,19 @@ open class TradingStateMachine( if (subaccountNumber != null) { if (staticTyping) { when (internalState.input.currentType) { - InternalInputType.Trade -> { + InputType.TRADE -> { calculateTrade(subaccountNumber) } - InternalInputType.Transfer -> { + InputType.TRADE -> { calculateTransfer(subaccountNumber) } - InternalInputType.TriggerOrder -> { + InputType.TRIGGER_ORDERS -> { calculateTriggerOrders(subaccountNumber) } - InternalInputType.AdjustIsolatedMargin -> { + InputType.ADJUST_ISOLATED_MARGIN -> { calculateAdjustIsolatedMargin(subaccountNumber) } - InternalInputType.ClosePosition -> { + InputType.CLOSE_POSITION -> { calculateClosePosition(subaccountNumber) } else -> {} diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/payload/TradeInputOptionsTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/payload/TradeInputOptionsTests.kt index 2153ddd6e..33c701b72 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/payload/TradeInputOptionsTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/payload/TradeInputOptionsTests.kt @@ -1,10 +1,15 @@ package exchange.dydx.abacus.payload +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.SelectionOption 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.state.model.tradeInMarket import kotlin.test.Test +import kotlin.test.assertEquals class TradeInputOptionsTests : V4BaseTests() { @Test @@ -15,11 +20,42 @@ class TradeInputOptionsTests : V4BaseTests() { } private fun testTradeInputOnce() { - test( - { - perp.tradeInMarket("ETH-USD", 0) - }, - """ + if (perp.staticTyping) { + perp.tradeInMarket("ETH-USD", 0) + assertEquals(perp.internalState.input.currentType, InputType.TRADE) + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.Limit) + assertEquals(trade.side, OrderSide.Buy) + assertEquals(trade.marketId, "ETH-USD") + assertEquals(trade.timeInForce, "GTT") + assertEquals(trade.options.needsPostOnly, true) + assertEquals(trade.options.marginModeOptions?.size, 2) + assertEquals( + trade.options.marginModeOptions?.get(0), + SelectionOption( + type = "CROSS", + stringKey = "APP.TRADE.CROSS_MARGIN", + string = null, + iconUrl = null + ) + ) + assertEquals( + trade.options.marginModeOptions?.get(1), + SelectionOption( + type = "ISOLATED", + stringKey = "APP.TRADE.ISOLATED_MARGIN", + string = null, + iconUrl = null + ) + ) + + + } else { + test( + { + perp.tradeInMarket("ETH-USD", 0) + }, + """ { "input": { "current": "trade", @@ -45,13 +81,21 @@ class TradeInputOptionsTests : V4BaseTests() { } } """.trimIndent(), - ) + ) + } - test( - { - perp.trade(null, null, 0) - }, - """ + if (perp.staticTyping) { + perp.trade(null, null, 0) + assertEquals(perp.internalState.input.currentType, InputType.TRADE) + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.Limit) + assertEquals(trade.side, OrderSide.Buy) + } else { + test( + { + perp.trade(null, null, 0) + }, + """ { "input": { "current": "trade", @@ -63,17 +107,41 @@ class TradeInputOptionsTests : V4BaseTests() { } } """.trimIndent(), - ) + ) + } - test({ + if (perp.staticTyping) { perp.trade("BUY", TradeInputField.side, 0) - }, null) + assertEquals(perp.internalState.input.currentType, InputType.TRADE) + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.Limit) + assertEquals(trade.side, OrderSide.Buy) + } else { + test({ + perp.trade("BUY", TradeInputField.side, 0) + }, null) - test( - { - perp.trade("MARKET", TradeInputField.type, 0) - }, - """ + } + + if (perp.staticTyping) { + perp.trade("MARKET", TradeInputField.type, 0) + val trade = perp.internalState.input.trade + val options = trade.options + assertEquals(options.needsSize, true) + assertEquals(options.needsLeverage, true) + assertEquals(options.needsTriggerPrice, false) + assertEquals(options.needsLimitPrice, false) + assertEquals(options.needsTrailingPercent, false) + assertEquals(options.needsReduceOnly, true) + assertEquals(options.needsPostOnly, false) + assertEquals(options.needsBrackets, true) + assertEquals(options.needsGoodUntil, false) + } else { + test( + { + perp.trade("MARKET", TradeInputField.type, 0) + }, + """ { "input": { "trade": { @@ -94,13 +162,28 @@ class TradeInputOptionsTests : V4BaseTests() { } } """.trimIndent(), - ) + ) + } - test( - { - perp.trade("LIMIT", TradeInputField.type, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("LIMIT", TradeInputField.type, 0) + val trade = perp.internalState.input.trade + val options = trade.options + assertEquals(options.needsSize, true) + assertEquals(options.needsLeverage, false) + assertEquals(options.needsTriggerPrice, false) + assertEquals(options.needsLimitPrice, true) + assertEquals(options.needsTrailingPercent, false) + assertEquals(options.needsReduceOnly, false) + assertEquals(options.needsPostOnly, true) + assertEquals(options.needsBrackets, false) + assertEquals(options.needsGoodUntil, true) + } else { + test( + { + perp.trade("LIMIT", TradeInputField.type, 0) + }, + """ { "input": { "trade": { @@ -121,13 +204,28 @@ class TradeInputOptionsTests : V4BaseTests() { } } """.trimIndent(), - ) + ) + } - test( - { - perp.trade("GTT", TradeInputField.timeInForceType, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("GTT", TradeInputField.timeInForceType, 0) + val trade = perp.internalState.input.trade + val options = trade.options + assertEquals(options.needsSize, true) + assertEquals(options.needsLeverage, false) + assertEquals(options.needsTriggerPrice, false) + assertEquals(options.needsLimitPrice, true) + assertEquals(options.needsTrailingPercent, false) + assertEquals(options.needsReduceOnly, false) + assertEquals(options.needsPostOnly, true) + assertEquals(options.needsBrackets, false) + assertEquals(options.needsGoodUntil, true) + } else { + test( + { + perp.trade("GTT", TradeInputField.timeInForceType, 0) + }, + """ { "input": { "trade": { @@ -148,13 +246,47 @@ class TradeInputOptionsTests : V4BaseTests() { } } """.trimIndent(), - ) + ) + } - test( - { - perp.trade("IOC", TradeInputField.timeInForceType, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("GTT", TradeInputField.timeInForceType, 0) + val trade = perp.internalState.input.trade + val options = trade.options + assertEquals(options.needsSize, true) + assertEquals(options.needsLeverage, false) + assertEquals(options.needsTriggerPrice, false) + assertEquals(options.needsLimitPrice, true) + assertEquals(options.needsTrailingPercent, false) + assertEquals(options.needsReduceOnly, false) + assertEquals(options.needsPostOnly, true) + assertEquals(options.needsBrackets, false) + assertEquals(options.needsGoodUntil, true) + assertEquals(options.timeInForceOptions?.size, 2) + assertEquals( + options.timeInForceOptions?.get(0), + SelectionOption( + type = "GTT", + stringKey = "APP.TRADE.GOOD_TIL_TIME", + string = null, + iconUrl = null + ) + ) + assertEquals( + options.timeInForceOptions?.get(1), + SelectionOption( + type = "IOC", + stringKey = "APP.TRADE.IMMEDIATE_OR_CANCEL", + string = null, + iconUrl = null + ) + ) + } else { + test( + { + perp.trade("IOC", TradeInputField.timeInForceType, 0) + }, + """ { "input": { "trade": { @@ -185,6 +317,7 @@ class TradeInputOptionsTests : V4BaseTests() { } } """.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 ecfb19b83..33b405280 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4NoAccountTradeInputTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4NoAccountTradeInputTests.kt @@ -5,6 +5,7 @@ import exchange.dydx.abacus.state.model.trade import exchange.dydx.abacus.state.model.tradeInMarket import exchange.dydx.abacus.tests.extensions.loadOrderbook import kotlin.test.Test +import kotlin.test.assertEquals class V4NoAccountTradeInputTests : V4BaseTests() { @Test @@ -30,11 +31,15 @@ class V4NoAccountTradeInputTests : V4BaseTests() { } private fun testOnce() { - test( - { - perp.tradeInMarket("ETH-USD", 0) - }, - """ + if (perp.staticTyping) { + perp.tradeInMarket("ETH-USD", 0) + assertEquals(perp.internalState.input.trade.marketId, "ETH-USD") + } else { + test( + { + perp.tradeInMarket("ETH-USD", 0) + }, + """ { "input": { "trade": { @@ -43,7 +48,8 @@ class V4NoAccountTradeInputTests : V4BaseTests() { } } """.trimIndent(), - ) + ) + } test({ perp.trade("LIMIT", TradeInputField.type, 0) From ab7858f6ea6bfd422ec6205a40d8e2ae944db1a0 Mon Sep 17 00:00:00 2001 From: mobile-build-bot-git Date: Tue, 13 Aug 2024 20:57:32 +0000 Subject: [PATCH 30/63] Bump version --- build.gradle.kts | 2 +- v4_abacus.podspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 8f419a7b2..3ac7ed2d5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -52,7 +52,7 @@ allprojects { } group = "exchange.dydx.abacus" -version = "1.8.84" +version = "1.8.85" repositories { google() diff --git a/v4_abacus.podspec b/v4_abacus.podspec index 83210976a..f16d0e235 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.84' + spec.version = '1.8.85' spec.homepage = 'https://github.com/dydxprotocol/v4-abacus' spec.source = { :http=> ''} spec.authors = '' From 75007fc9b090d26da07066c0b74e764d63840033 Mon Sep 17 00:00:00 2001 From: Rui Date: Tue, 13 Aug 2024 14:01:50 -0700 Subject: [PATCH 31/63] Lint --- .../V2/TradeInput/TradeInputCalculatorV2.kt | 2 +- .../TradeInputMarginModeCalculator.kt | 2 +- .../TradeInputMarketOrderCalculator.kt | 2 +- .../TradeInputNonMarketOrderCalculator.kt | 2 +- .../TradeInput/TradeInputOptionsCalculator.kt | 12 +++---- .../TradeInput/TradeInputSummaryCalculator.kt | 2 +- .../output/input/TradeInput.kt | 1 - .../input/TradeInputField+Actions.kt | 15 +++----- .../processor/input/TradeInputProcessor.kt | 35 +++---------------- .../state/model/TradingStateMachine.kt | 2 +- .../payload/TradeInputOptionsTests.kt | 31 ++++++++-------- .../payload/v4/V4NoAccountTradeInputTests.kt | 2 +- 12 files changed, 36 insertions(+), 72 deletions(-) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputCalculatorV2.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputCalculatorV2.kt index 4defca71c..8c7192f0a 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputCalculatorV2.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputCalculatorV2.kt @@ -1,4 +1,4 @@ -package exchange.dydx.abacus.calculator.v2.TradeInput +package exchange.dydx.abacus.calculator.v2.tradeInput import exchange.dydx.abacus.calculator.TradeCalculation import exchange.dydx.abacus.output.FeeTier diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputMarginModeCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputMarginModeCalculator.kt index 2e2c37910..f7cb01040 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputMarginModeCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputMarginModeCalculator.kt @@ -1,4 +1,4 @@ -package exchange.dydx.abacus.calculator.v2.TradeInput +package exchange.dydx.abacus.calculator.v2.tradeInput import exchange.dydx.abacus.calculator.CalculationPeriod import exchange.dydx.abacus.calculator.MarginCalculator 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 cdbecc1d5..1721ee79b 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 @@ -1,6 +1,6 @@ @file:Suppress("ktlint:standard:property-naming") -package exchange.dydx.abacus.calculator.v2.TradeInput +package exchange.dydx.abacus.calculator.v2.tradeInput import abs import exchange.dydx.abacus.calculator.CalculationPeriod diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputNonMarketOrderCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputNonMarketOrderCalculator.kt index e785c1c39..65d661c46 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputNonMarketOrderCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputNonMarketOrderCalculator.kt @@ -1,4 +1,4 @@ -package exchange.dydx.abacus.calculator.v2.TradeInput +package exchange.dydx.abacus.calculator.v2.tradeInput import exchange.dydx.abacus.output.input.OrderType import exchange.dydx.abacus.output.input.TradeInputPrice diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputOptionsCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputOptionsCalculator.kt index 7a8408cdc..0db95fab4 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputOptionsCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputOptionsCalculator.kt @@ -1,4 +1,4 @@ -package exchange.dydx.abacus.calculator.v2.TradeInput +package exchange.dydx.abacus.calculator.v2.tradeInput import exchange.dydx.abacus.calculator.CalculationPeriod import exchange.dydx.abacus.calculator.MarginCalculator @@ -122,7 +122,7 @@ internal class TradeInputOptionsCalculator( limitPriceField(), triggerPriceField(), goodTilField(), - executionField(true), + executionField(includesDefaultAndPostOnly = true), marginModeField(market, account, subaccount), when (execution) { "IOC" -> reduceOnlyField() @@ -136,7 +136,7 @@ internal class TradeInputOptionsCalculator( sizeField(), triggerPriceField(), goodTilField(), - executionField(false), + executionField(includesDefaultAndPostOnly = false), marginModeField(market, account, subaccount), reduceOnlyField(), ).filterNotNull() @@ -147,7 +147,7 @@ internal class TradeInputOptionsCalculator( sizeField(), trailingPercentField(), goodTilField(), - executionField(false), + executionField(includesDefaultAndPostOnly = false), marginModeField(market, account, subaccount), ).filterNotNull() } @@ -297,7 +297,7 @@ internal class TradeInputOptionsCalculator( market = market, ) if (options.needsGoodUntil) { - if (trade.goodTil == null) { + if (trade.goodTil?.duration == null) { val goodTil = TradeInputGoodUntil.safeCreate(trade.goodTil) trade.goodTil = goodTil.copy(duration = 28.0) } @@ -426,7 +426,7 @@ internal class TradeInputOptionsCalculator( stopLossField(), takeProfitField(), goodTilField(), - executionField(false), + executionField(includesDefaultAndPostOnly = false), ), ) } 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 fb281b751..0cf501513 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 @@ -1,4 +1,4 @@ -package exchange.dydx.abacus.calculator.v2.TradeInput +package exchange.dydx.abacus.calculator.v2.tradeInput import abs import exchange.dydx.abacus.calculator.CalculationPeriod 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 49ddbf8e6..903f15b8d 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/output/input/TradeInput.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/TradeInput.kt @@ -1,7 +1,6 @@ package exchange.dydx.abacus.output.input import exchange.dydx.abacus.protocols.ParserProtocol -import exchange.dydx.abacus.state.internalstate.InternalMarketSummaryState import exchange.dydx.abacus.state.internalstate.InternalTradeInputOptions import exchange.dydx.abacus.state.internalstate.InternalTradeInputState import exchange.dydx.abacus.state.internalstate.InternalTradeInputSummary diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputField+Actions.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputField+Actions.kt index 4eaabb8a8..c4c31e649 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputField+Actions.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputField+Actions.kt @@ -122,18 +122,15 @@ internal val TradeInputField.updateValueAction: ((InternalTradeInputState, Strin } limitPrice -> { trade, value, parser -> - trade.price = - TradeInputPrice.safeCreate(trade.price).copy(limitPrice = parser.asDouble(value)) + trade.price = TradeInputPrice.safeCreate(trade.price).copy(limitPrice = parser.asDouble(value)) } triggerPrice -> { trade, value, parser -> - trade.price = - TradeInputPrice.safeCreate(trade.price).copy(triggerPrice = parser.asDouble(value)) + trade.price = TradeInputPrice.safeCreate(trade.price).copy(triggerPrice = parser.asDouble(value)) } trailingPercent -> { trade, value, parser -> - trade.price = TradeInputPrice.safeCreate(trade.price) - .copy(trailingPercent = parser.asDouble(value)) + trade.price = TradeInputPrice.safeCreate(trade.price).copy(trailingPercent = parser.asDouble(value)) } bracketsStopLossPrice -> { trade, value, parser -> @@ -237,12 +234,10 @@ internal val TradeInputField.updateValueAction: ((InternalTradeInputState, Strin } usdcSize -> { trade, value, parser -> - trade.size = - TradeInputSize.safeCreate(trade.size).copy(usdcSize = parser.asDouble(value)) + trade.size = TradeInputSize.safeCreate(trade.size).copy(usdcSize = parser.asDouble(value)) } leverage -> { trade, value, parser -> - trade.size = - TradeInputSize.safeCreate(trade.size).copy(leverage = parser.asDouble(value)) + trade.size = TradeInputSize.safeCreate(trade.size).copy(leverage = parser.asDouble(value)) } } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputProcessor.kt index f520e9982..f1c6af077 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputProcessor.kt @@ -4,8 +4,7 @@ import abs import exchange.dydx.abacus.calculator.CalculationPeriod import exchange.dydx.abacus.calculator.MarginCalculator import exchange.dydx.abacus.calculator.TradeCalculation -import exchange.dydx.abacus.calculator.TradeInputCalculator -import exchange.dydx.abacus.calculator.v2.TradeInput.TradeInputCalculatorV2 +import exchange.dydx.abacus.calculator.v2.tradeInput.TradeInputCalculatorV2 import exchange.dydx.abacus.output.PerpetualMarketType import exchange.dydx.abacus.output.input.InputType import exchange.dydx.abacus.output.input.MarginMode @@ -28,33 +27,7 @@ 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.model.TradeInputField -import exchange.dydx.abacus.state.model.TradeInputField.bracketsExecution -import exchange.dydx.abacus.state.model.TradeInputField.bracketsGoodUntilDuration -import exchange.dydx.abacus.state.model.TradeInputField.bracketsGoodUntilUnit -import exchange.dydx.abacus.state.model.TradeInputField.bracketsStopLossPercent -import exchange.dydx.abacus.state.model.TradeInputField.bracketsStopLossPrice -import exchange.dydx.abacus.state.model.TradeInputField.bracketsStopLossReduceOnly -import exchange.dydx.abacus.state.model.TradeInputField.bracketsTakeProfitPercent -import exchange.dydx.abacus.state.model.TradeInputField.bracketsTakeProfitPrice -import exchange.dydx.abacus.state.model.TradeInputField.bracketsTakeProfitReduceOnly -import exchange.dydx.abacus.state.model.TradeInputField.execution -import exchange.dydx.abacus.state.model.TradeInputField.goodTilDuration -import exchange.dydx.abacus.state.model.TradeInputField.goodTilUnit -import exchange.dydx.abacus.state.model.TradeInputField.leverage -import exchange.dydx.abacus.state.model.TradeInputField.limitPrice -import exchange.dydx.abacus.state.model.TradeInputField.marginMode -import exchange.dydx.abacus.state.model.TradeInputField.postOnly -import exchange.dydx.abacus.state.model.TradeInputField.reduceOnly -import exchange.dydx.abacus.state.model.TradeInputField.side -import exchange.dydx.abacus.state.model.TradeInputField.size -import exchange.dydx.abacus.state.model.TradeInputField.targetLeverage -import exchange.dydx.abacus.state.model.TradeInputField.timeInForceType -import exchange.dydx.abacus.state.model.TradeInputField.trailingPercent -import exchange.dydx.abacus.state.model.TradeInputField.triggerPrice -import exchange.dydx.abacus.state.model.TradeInputField.type -import exchange.dydx.abacus.state.model.TradeInputField.usdcSize import kollections.iListOf -import kotlin.math.abs internal interface TradeInputProcessorProtocol { fun tradeInMarket( @@ -149,7 +122,7 @@ internal class TradeInputProcessor( ) } - override fun trade( + override fun trade( inputState: InternalInputState, walletState: InternalWalletState, marketSummaryState: InternalMarketSummaryState, @@ -279,12 +252,12 @@ internal class TradeInputProcessor( } if (sizeChanged) { - when (type) { + when (inputType) { TradeInputField.size, TradeInputField.usdcSize, TradeInputField.leverage, -> { - TradeInputSize.safeCreate(trade.size).copy(input = type.rawValue) + trade.size = TradeInputSize.safeCreate(trade.size).copy(input = inputType.rawValue) } else -> {} } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt index c24d087bd..aed702792 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt @@ -9,7 +9,7 @@ import exchange.dydx.abacus.calculator.TradeInputCalculator import exchange.dydx.abacus.calculator.TransferInputCalculator import exchange.dydx.abacus.calculator.TriggerOrdersInputCalculator import exchange.dydx.abacus.calculator.v2.AccountCalculatorV2 -import exchange.dydx.abacus.calculator.v2.TradeInput.TradeInputCalculatorV2 +import exchange.dydx.abacus.calculator.v2.tradeInput.TradeInputCalculatorV2 import exchange.dydx.abacus.output.Asset import exchange.dydx.abacus.output.Configs import exchange.dydx.abacus.output.LaunchIncentive diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/payload/TradeInputOptionsTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/payload/TradeInputOptionsTests.kt index 33c701b72..02bfbf7b1 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/payload/TradeInputOptionsTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/payload/TradeInputOptionsTests.kt @@ -36,8 +36,8 @@ class TradeInputOptionsTests : V4BaseTests() { type = "CROSS", stringKey = "APP.TRADE.CROSS_MARGIN", string = null, - iconUrl = null - ) + iconUrl = null, + ), ) assertEquals( trade.options.marginModeOptions?.get(1), @@ -45,11 +45,9 @@ class TradeInputOptionsTests : V4BaseTests() { type = "ISOLATED", stringKey = "APP.TRADE.ISOLATED_MARGIN", string = null, - iconUrl = null - ) + iconUrl = null, + ), ) - - } else { test( { @@ -80,7 +78,7 @@ class TradeInputOptionsTests : V4BaseTests() { } } } - """.trimIndent(), + """.trimIndent(), ) } @@ -106,7 +104,7 @@ class TradeInputOptionsTests : V4BaseTests() { } } } - """.trimIndent(), + """.trimIndent(), ) } @@ -120,7 +118,6 @@ class TradeInputOptionsTests : V4BaseTests() { test({ perp.trade("BUY", TradeInputField.side, 0) }, null) - } if (perp.staticTyping) { @@ -161,7 +158,7 @@ class TradeInputOptionsTests : V4BaseTests() { } } } - """.trimIndent(), + """.trimIndent(), ) } @@ -203,7 +200,7 @@ class TradeInputOptionsTests : V4BaseTests() { } } } - """.trimIndent(), + """.trimIndent(), ) } @@ -245,7 +242,7 @@ class TradeInputOptionsTests : V4BaseTests() { } } } - """.trimIndent(), + """.trimIndent(), ) } @@ -269,8 +266,8 @@ class TradeInputOptionsTests : V4BaseTests() { type = "GTT", stringKey = "APP.TRADE.GOOD_TIL_TIME", string = null, - iconUrl = null - ) + iconUrl = null, + ), ) assertEquals( options.timeInForceOptions?.get(1), @@ -278,8 +275,8 @@ class TradeInputOptionsTests : V4BaseTests() { type = "IOC", stringKey = "APP.TRADE.IMMEDIATE_OR_CANCEL", string = null, - iconUrl = null - ) + iconUrl = null, + ), ) } else { test( @@ -316,7 +313,7 @@ class TradeInputOptionsTests : 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 33b405280..f9e9641f1 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4NoAccountTradeInputTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v4/V4NoAccountTradeInputTests.kt @@ -47,7 +47,7 @@ class V4NoAccountTradeInputTests : V4BaseTests() { } } } - """.trimIndent(), + """.trimIndent(), ) } From 1975e726d4274b5fad8045cd54e31c644c31968c Mon Sep 17 00:00:00 2001 From: Rui Date: Tue, 13 Aug 2024 14:13:09 -0700 Subject: [PATCH 32/63] Lint --- .../V2/TradeInput/TradeInputCalculatorV2.kt | 2 +- .../TradeInputMarginModeCalculator.kt | 2 +- .../TradeInputMarketOrderCalculator.kt | 2 +- .../TradeInputNonMarketOrderCalculator.kt | 2 +- .../TradeInput/TradeInputOptionsCalculator.kt | 2 +- .../TradeInput/TradeInputSummaryCalculator.kt | 2 +- .../input/TradeInputField+Actions.kt | 173 ++++++++---------- .../processor/input/TradeInputProcessor.kt | 2 +- .../state/model/TradingStateMachine.kt | 2 +- 9 files changed, 82 insertions(+), 107 deletions(-) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputCalculatorV2.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputCalculatorV2.kt index 8c7192f0a..b4a3a5ad3 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputCalculatorV2.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputCalculatorV2.kt @@ -1,4 +1,4 @@ -package exchange.dydx.abacus.calculator.v2.tradeInput +package exchange.dydx.abacus.calculator.v2.tradeinput import exchange.dydx.abacus.calculator.TradeCalculation import exchange.dydx.abacus.output.FeeTier diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputMarginModeCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputMarginModeCalculator.kt index f7cb01040..4b0dc151a 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputMarginModeCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputMarginModeCalculator.kt @@ -1,4 +1,4 @@ -package exchange.dydx.abacus.calculator.v2.tradeInput +package exchange.dydx.abacus.calculator.v2.tradeinput import exchange.dydx.abacus.calculator.CalculationPeriod import exchange.dydx.abacus.calculator.MarginCalculator 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 1721ee79b..80afb1554 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 @@ -1,6 +1,6 @@ @file:Suppress("ktlint:standard:property-naming") -package exchange.dydx.abacus.calculator.v2.tradeInput +package exchange.dydx.abacus.calculator.v2.tradeinput import abs import exchange.dydx.abacus.calculator.CalculationPeriod diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputNonMarketOrderCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputNonMarketOrderCalculator.kt index 65d661c46..d92fc4e3a 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputNonMarketOrderCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputNonMarketOrderCalculator.kt @@ -1,4 +1,4 @@ -package exchange.dydx.abacus.calculator.v2.tradeInput +package exchange.dydx.abacus.calculator.v2.tradeinput import exchange.dydx.abacus.output.input.OrderType import exchange.dydx.abacus.output.input.TradeInputPrice diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputOptionsCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputOptionsCalculator.kt index 0db95fab4..ecb06638e 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputOptionsCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputOptionsCalculator.kt @@ -1,4 +1,4 @@ -package exchange.dydx.abacus.calculator.v2.tradeInput +package exchange.dydx.abacus.calculator.v2.tradeinput import exchange.dydx.abacus.calculator.CalculationPeriod import exchange.dydx.abacus.calculator.MarginCalculator 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 0cf501513..4fe01fccc 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 @@ -1,4 +1,4 @@ -package exchange.dydx.abacus.calculator.v2.tradeInput +package exchange.dydx.abacus.calculator.v2.tradeinput import abs import exchange.dydx.abacus.calculator.CalculationPeriod diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputField+Actions.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputField+Actions.kt index c4c31e649..c4257c16a 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputField+Actions.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputField+Actions.kt @@ -12,31 +12,6 @@ import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.internalstate.InternalTradeInputState import exchange.dydx.abacus.state.internalstate.safeCreate import exchange.dydx.abacus.state.model.TradeInputField -import exchange.dydx.abacus.state.model.TradeInputField.bracketsExecution -import exchange.dydx.abacus.state.model.TradeInputField.bracketsGoodUntilDuration -import exchange.dydx.abacus.state.model.TradeInputField.bracketsGoodUntilUnit -import exchange.dydx.abacus.state.model.TradeInputField.bracketsStopLossPercent -import exchange.dydx.abacus.state.model.TradeInputField.bracketsStopLossPrice -import exchange.dydx.abacus.state.model.TradeInputField.bracketsStopLossReduceOnly -import exchange.dydx.abacus.state.model.TradeInputField.bracketsTakeProfitPercent -import exchange.dydx.abacus.state.model.TradeInputField.bracketsTakeProfitPrice -import exchange.dydx.abacus.state.model.TradeInputField.bracketsTakeProfitReduceOnly -import exchange.dydx.abacus.state.model.TradeInputField.execution -import exchange.dydx.abacus.state.model.TradeInputField.goodTilDuration -import exchange.dydx.abacus.state.model.TradeInputField.goodTilUnit -import exchange.dydx.abacus.state.model.TradeInputField.leverage -import exchange.dydx.abacus.state.model.TradeInputField.limitPrice -import exchange.dydx.abacus.state.model.TradeInputField.marginMode -import exchange.dydx.abacus.state.model.TradeInputField.postOnly -import exchange.dydx.abacus.state.model.TradeInputField.reduceOnly -import exchange.dydx.abacus.state.model.TradeInputField.side -import exchange.dydx.abacus.state.model.TradeInputField.size -import exchange.dydx.abacus.state.model.TradeInputField.targetLeverage -import exchange.dydx.abacus.state.model.TradeInputField.timeInForceType -import exchange.dydx.abacus.state.model.TradeInputField.trailingPercent -import exchange.dydx.abacus.state.model.TradeInputField.triggerPrice -import exchange.dydx.abacus.state.model.TradeInputField.type -import exchange.dydx.abacus.state.model.TradeInputField.usdcSize // // Contains the helper functions for each of the trade input fields @@ -45,75 +20,75 @@ import exchange.dydx.abacus.state.model.TradeInputField.usdcSize // Returns the validation action for the trade input field internal val TradeInputField.validTradeInputAction: ((InternalTradeInputState) -> Boolean)? get() = when (this) { - type, side -> null - size, usdcSize, leverage -> { it -> it.options.needsSize } - limitPrice -> { it -> it.options.needsLimitPrice } - triggerPrice -> { it -> it.options.needsTriggerPrice } - trailingPercent -> { it -> it.options.needsTrailingPercent } - targetLeverage -> { it -> it.options.needsTargetLeverage } - goodTilDuration, goodTilUnit -> { it -> it.options.needsGoodUntil } - reduceOnly -> { it -> it.options.needsReduceOnly } - postOnly -> { it -> it.options.needsPostOnly } - bracketsStopLossPrice, - bracketsStopLossPercent, - bracketsTakeProfitPrice, - bracketsTakeProfitPercent, - bracketsGoodUntilDuration, - bracketsGoodUntilUnit, - bracketsStopLossReduceOnly, - bracketsTakeProfitReduceOnly, - bracketsExecution -> { it -> it.options.needsBrackets } - timeInForceType -> { it -> it.options.timeInForceOptions != null } - execution -> { it -> it.options.executionOptions != null } - marginMode -> { it -> it.options.marginModeOptions != null } + TradeInputField.type, TradeInputField.side -> null + TradeInputField.size, TradeInputField.usdcSize, TradeInputField.leverage -> { it -> it.options.needsSize } + TradeInputField.limitPrice -> { it -> it.options.needsLimitPrice } + TradeInputField.triggerPrice -> { it -> it.options.needsTriggerPrice } + TradeInputField.trailingPercent -> { it -> it.options.needsTrailingPercent } + TradeInputField.targetLeverage -> { it -> it.options.needsTargetLeverage } + TradeInputField.goodTilDuration, TradeInputField.goodTilUnit -> { it -> it.options.needsGoodUntil } + TradeInputField.reduceOnly -> { it -> it.options.needsReduceOnly } + TradeInputField.postOnly -> { it -> it.options.needsPostOnly } + TradeInputField.bracketsStopLossPrice, + TradeInputField.bracketsStopLossPercent, + TradeInputField.bracketsTakeProfitPrice, + TradeInputField.bracketsTakeProfitPercent, + TradeInputField.bracketsGoodUntilDuration, + TradeInputField.bracketsGoodUntilUnit, + TradeInputField.bracketsStopLossReduceOnly, + TradeInputField.bracketsTakeProfitReduceOnly, + TradeInputField.bracketsExecution -> { it -> it.options.needsBrackets } + TradeInputField.timeInForceType -> { it -> it.options.timeInForceOptions != null } + TradeInputField.execution -> { it -> it.options.executionOptions != null } + TradeInputField.marginMode -> { it -> it.options.marginModeOptions != null } TradeInputField.lastInput -> { it -> true } } // Returns the action to read value for the trade input field internal val TradeInputField.valueAction: ((InternalTradeInputState) -> Any?)? get() = when (this) { - type -> { it -> it.type } - side -> { it -> it.side } + TradeInputField.type -> { it -> it.type } + TradeInputField.side -> { it -> it.side } - marginMode -> { it -> it.marginMode } - targetLeverage -> { it -> it.targetLeverage } + TradeInputField.marginMode -> { it -> it.marginMode } + TradeInputField.targetLeverage -> { it -> it.targetLeverage } - size -> { it -> it.size?.size } - usdcSize -> { it -> it.size?.usdcSize } - leverage -> { it -> it.size?.leverage } + TradeInputField.size -> { it -> it.size?.size } + TradeInputField.usdcSize -> { it -> it.size?.usdcSize } + TradeInputField.leverage -> { it -> it.size?.leverage } TradeInputField.lastInput -> { it -> it.size?.input } - limitPrice -> { it -> it.price?.limitPrice } - triggerPrice -> { it -> it.price?.triggerPrice } - trailingPercent -> { it -> it.price?.trailingPercent } - - timeInForceType -> { it -> it.timeInForce } - goodTilDuration -> { it -> it.goodTil?.duration } - goodTilUnit -> { it -> it.goodTil?.unit } - - execution -> { it -> it.execution } - reduceOnly -> { it -> it.reduceOnly } - postOnly -> { it -> it.postOnly } - - bracketsStopLossPrice -> { it -> it.bracket?.stopLoss?.triggerPrice } - bracketsStopLossPercent -> { it -> it.bracket?.stopLoss?.percent } - bracketsStopLossReduceOnly -> { it -> it.bracket?.stopLoss?.reduceOnly } - bracketsTakeProfitPrice -> { it -> it.bracket?.takeProfit?.triggerPrice } - bracketsTakeProfitPercent -> { it -> it.bracket?.takeProfit?.percent } - bracketsTakeProfitReduceOnly -> { it -> it.bracket?.takeProfit?.reduceOnly } - bracketsGoodUntilDuration -> { it -> it.bracket?.goodTil?.duration } - bracketsGoodUntilUnit -> { it -> it.bracket?.goodTil?.unit } - bracketsExecution -> { it -> it.bracket?.execution } + TradeInputField.limitPrice -> { it -> it.price?.limitPrice } + TradeInputField.triggerPrice -> { it -> it.price?.triggerPrice } + TradeInputField.trailingPercent -> { it -> it.price?.trailingPercent } + + TradeInputField.timeInForceType -> { it -> it.timeInForce } + TradeInputField.goodTilDuration -> { it -> it.goodTil?.duration } + TradeInputField.goodTilUnit -> { it -> it.goodTil?.unit } + + TradeInputField.execution -> { it -> it.execution } + TradeInputField.reduceOnly -> { it -> it.reduceOnly } + TradeInputField.postOnly -> { it -> it.postOnly } + + TradeInputField.bracketsStopLossPrice -> { it -> it.bracket?.stopLoss?.triggerPrice } + TradeInputField.bracketsStopLossPercent -> { it -> it.bracket?.stopLoss?.percent } + TradeInputField.bracketsStopLossReduceOnly -> { it -> it.bracket?.stopLoss?.reduceOnly } + TradeInputField.bracketsTakeProfitPrice -> { it -> it.bracket?.takeProfit?.triggerPrice } + TradeInputField.bracketsTakeProfitPercent -> { it -> it.bracket?.takeProfit?.percent } + TradeInputField.bracketsTakeProfitReduceOnly -> { it -> it.bracket?.takeProfit?.reduceOnly } + TradeInputField.bracketsGoodUntilDuration -> { it -> it.bracket?.goodTil?.duration } + TradeInputField.bracketsGoodUntilUnit -> { it -> it.bracket?.goodTil?.unit } + TradeInputField.bracketsExecution -> { it -> it.bracket?.execution } } // Returns the write action to update value for the trade input field internal val TradeInputField.updateValueAction: ((InternalTradeInputState, String?, ParserProtocol) -> Unit)? get() = when (this) { - type -> { trade, value, parser -> + TradeInputField.type -> { trade, value, parser -> trade.type = OrderType.invoke(value) } - side -> { trade, value, parser -> + TradeInputField.side -> { trade, value, parser -> trade.side = OrderSide.invoke(value) } @@ -121,78 +96,78 @@ internal val TradeInputField.updateValueAction: ((InternalTradeInputState, Strin trade.size = TradeInputSize.safeCreate(trade.size).copy(input = value) } - limitPrice -> { trade, value, parser -> + TradeInputField.limitPrice -> { trade, value, parser -> trade.price = TradeInputPrice.safeCreate(trade.price).copy(limitPrice = parser.asDouble(value)) } - triggerPrice -> { trade, value, parser -> + TradeInputField.triggerPrice -> { trade, value, parser -> trade.price = TradeInputPrice.safeCreate(trade.price).copy(triggerPrice = parser.asDouble(value)) } - trailingPercent -> { trade, value, parser -> + TradeInputField.trailingPercent -> { trade, value, parser -> trade.price = TradeInputPrice.safeCreate(trade.price).copy(trailingPercent = parser.asDouble(value)) } - bracketsStopLossPrice -> { trade, value, parser -> + TradeInputField.bracketsStopLossPrice -> { trade, value, parser -> val braket = TradeInputBracket.safeCreate(trade.bracket) val stopLoss = TradeInputBracketSide.safeCreate(braket.stopLoss) trade.bracket = braket.copy(stopLoss = stopLoss.copy(triggerPrice = parser.asDouble(value))) } - bracketsStopLossPercent -> { trade, value, parser -> + TradeInputField.bracketsStopLossPercent -> { trade, value, parser -> val braket = TradeInputBracket.safeCreate(trade.bracket) val stopLoss = TradeInputBracketSide.safeCreate(braket.stopLoss) trade.bracket = braket.copy(stopLoss = stopLoss.copy(percent = parser.asDouble(value))) } - bracketsTakeProfitPrice -> { trade, value, parser -> + TradeInputField.bracketsTakeProfitPrice -> { trade, value, parser -> val braket = TradeInputBracket.safeCreate(trade.bracket) val takeProfit = TradeInputBracketSide.safeCreate(braket.takeProfit) trade.bracket = braket.copy(takeProfit = takeProfit.copy(triggerPrice = parser.asDouble(value))) } - bracketsTakeProfitPercent -> { trade, value, parser -> + TradeInputField.bracketsTakeProfitPercent -> { trade, value, parser -> val braket = TradeInputBracket.safeCreate(trade.bracket) val takeProfit = TradeInputBracketSide.safeCreate(braket.takeProfit) trade.bracket = braket.copy(takeProfit = takeProfit.copy(percent = parser.asDouble(value))) } - marginMode -> { trade, value, parser -> + TradeInputField.marginMode -> { trade, value, parser -> trade.marginMode = MarginMode.invoke(value) } - timeInForceType -> { trade, value, parser -> + TradeInputField.timeInForceType -> { trade, value, parser -> trade.timeInForce = value } - goodTilUnit -> { trade, value, parser -> + TradeInputField.goodTilUnit -> { trade, value, parser -> trade.goodTil = TradeInputGoodUntil.safeCreate(trade.goodTil).copy(unit = value) } - bracketsGoodUntilUnit -> { trade, value, parser -> + TradeInputField.bracketsGoodUntilUnit -> { trade, value, parser -> val braket = TradeInputBracket.safeCreate(trade.bracket) trade.bracket = braket.copy( goodTil = TradeInputGoodUntil.safeCreate(braket.goodTil).copy(unit = value), ) } - execution -> { trade, value, parser -> + TradeInputField.execution -> { trade, value, parser -> trade.execution = value } - bracketsExecution -> { trade, value, parser -> + TradeInputField.bracketsExecution -> { trade, value, parser -> trade.bracket = TradeInputBracket.safeCreate(trade.bracket).copy(execution = value) } - goodTilDuration -> { trade, value, parser -> + TradeInputField.goodTilDuration -> { trade, value, parser -> trade.goodTil = TradeInputGoodUntil.safeCreate(trade.goodTil) .copy(duration = parser.asDouble(value)) } - bracketsGoodUntilDuration -> { trade, value, parser -> + TradeInputField.bracketsGoodUntilDuration -> { trade, value, parser -> val braket = TradeInputBracket.safeCreate(trade.bracket) trade.bracket = braket.copy( goodTil = TradeInputGoodUntil.safeCreate(braket.goodTil) @@ -200,22 +175,22 @@ internal val TradeInputField.updateValueAction: ((InternalTradeInputState, Strin ) } - reduceOnly -> { trade, value, parser -> + TradeInputField.reduceOnly -> { trade, value, parser -> trade.reduceOnly = parser.asBool(value) ?: false } - postOnly -> { trade, value, parser -> + TradeInputField.postOnly -> { trade, value, parser -> trade.postOnly = parser.asBool(value) ?: false } - bracketsStopLossReduceOnly -> { trade, value, parser -> + TradeInputField.bracketsStopLossReduceOnly -> { trade, value, parser -> val braket = TradeInputBracket.safeCreate(trade.bracket) val stopLoss = TradeInputBracketSide.safeCreate(braket.stopLoss) trade.bracket = braket.copy(stopLoss = stopLoss.copy(reduceOnly = parser.asBool(value) ?: false)) } - bracketsTakeProfitReduceOnly -> { trade, value, parser -> + TradeInputField.bracketsTakeProfitReduceOnly -> { trade, value, parser -> val braket = TradeInputBracket.safeCreate(trade.bracket) val takeProfit = TradeInputBracketSide.safeCreate(braket.takeProfit) trade.bracket = braket.copy( @@ -225,19 +200,19 @@ internal val TradeInputField.updateValueAction: ((InternalTradeInputState, Strin ) } - targetLeverage -> { trade, value, parser -> + TradeInputField.targetLeverage -> { trade, value, parser -> trade.targetLeverage = parser.asDouble(value) } - size -> { trade, value, parser -> + TradeInputField.size -> { trade, value, parser -> trade.size = TradeInputSize.safeCreate(trade.size).copy(size = parser.asDouble(value)) } - usdcSize -> { trade, value, parser -> + TradeInputField.usdcSize -> { trade, value, parser -> trade.size = TradeInputSize.safeCreate(trade.size).copy(usdcSize = parser.asDouble(value)) } - leverage -> { trade, value, parser -> + TradeInputField.leverage -> { trade, value, parser -> trade.size = TradeInputSize.safeCreate(trade.size).copy(leverage = parser.asDouble(value)) } } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputProcessor.kt index f1c6af077..fbf5ccf94 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputProcessor.kt @@ -4,7 +4,7 @@ import abs import exchange.dydx.abacus.calculator.CalculationPeriod import exchange.dydx.abacus.calculator.MarginCalculator import exchange.dydx.abacus.calculator.TradeCalculation -import exchange.dydx.abacus.calculator.v2.tradeInput.TradeInputCalculatorV2 +import exchange.dydx.abacus.calculator.v2.tradeinput.TradeInputCalculatorV2 import exchange.dydx.abacus.output.PerpetualMarketType import exchange.dydx.abacus.output.input.InputType import exchange.dydx.abacus.output.input.MarginMode diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt index aed702792..fb8175656 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt @@ -9,7 +9,7 @@ import exchange.dydx.abacus.calculator.TradeInputCalculator import exchange.dydx.abacus.calculator.TransferInputCalculator import exchange.dydx.abacus.calculator.TriggerOrdersInputCalculator import exchange.dydx.abacus.calculator.v2.AccountCalculatorV2 -import exchange.dydx.abacus.calculator.v2.tradeInput.TradeInputCalculatorV2 +import exchange.dydx.abacus.calculator.v2.tradeinput.TradeInputCalculatorV2 import exchange.dydx.abacus.output.Asset import exchange.dydx.abacus.output.Configs import exchange.dydx.abacus.output.LaunchIncentive From 952c97f52f8cfed2c12788ac8219765a5c2b4235 Mon Sep 17 00:00:00 2001 From: Rui Date: Tue, 13 Aug 2024 14:56:50 -0700 Subject: [PATCH 33/63] Move sides and order types options to a single source --- .../TradeInput/TradeInputOptionsCalculator.kt | 62 +++++++++++++++++++ .../output/input/TradeInput.kt | 6 +- .../state/internalstate/InternalState.kt | 2 + 3 files changed, 67 insertions(+), 3 deletions(-) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputOptionsCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputOptionsCalculator.kt index ecb06638e..d9578087b 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputOptionsCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputOptionsCalculator.kt @@ -3,6 +3,7 @@ package exchange.dydx.abacus.calculator.v2.tradeinput import exchange.dydx.abacus.calculator.CalculationPeriod import exchange.dydx.abacus.calculator.MarginCalculator import exchange.dydx.abacus.output.input.MarginMode +import exchange.dydx.abacus.output.input.OrderSide import exchange.dydx.abacus.output.input.OrderType import exchange.dydx.abacus.output.input.SelectionOption import exchange.dydx.abacus.output.input.Tooltip @@ -15,6 +16,7 @@ import exchange.dydx.abacus.state.internalstate.InternalSubaccountState import exchange.dydx.abacus.state.internalstate.InternalTradeInputOptions import exchange.dydx.abacus.state.internalstate.InternalTradeInputState import exchange.dydx.abacus.state.internalstate.safeCreate +import kollections.iListOf internal class TradeInputOptionsCalculator( private val parser: ParserProtocol, @@ -303,6 +305,11 @@ internal class TradeInputOptionsCalculator( } } + options.sideOptions = sideOptions + options.orderTypeOptions = orderTypeOptions + + trade.options = options + return trade } @@ -607,4 +614,59 @@ internal class TradeInputOptionsCalculator( string = null, iconUrl = null, ) + + private val sideOptions = listOf( + SelectionOption( + type = OrderSide.Buy.rawValue, + string = null, + stringKey = "APP.GENERAL.BUY", + iconUrl = null, + ), + SelectionOption( + type = OrderSide.Sell.rawValue, + string = null, + stringKey = "APP.GENERAL.SELL", + iconUrl = null, + ), + ) + + val orderTypeOptions = + iListOf( + SelectionOption( + type = OrderType.Limit.rawValue, + string = null, + stringKey = "APP.TRADE.LIMIT_ORDER_SHORT", + iconUrl = null, + ), + SelectionOption( + type = OrderType.Market.rawValue, + string = null, + stringKey = "APP.TRADE.MARKET_ORDER_SHORT", + iconUrl = null, + ), + SelectionOption( + type = OrderType.StopLimit.rawValue, + string = null, + stringKey = "APP.TRADE.STOP_LIMIT", + iconUrl = null, + ), + SelectionOption( + type = OrderType.StopMarket.rawValue, + string = null, + stringKey = "APP.TRADE.STOP_MARKET", + iconUrl = null, + ), + SelectionOption( + type = OrderType.TakeProfitLimit.rawValue, + string = null, + stringKey = "APP.TRADE.TAKE_PROFIT", + iconUrl = null, + ), + SelectionOption( + type = OrderType.TakeProfitMarket.rawValue, + string = null, + stringKey = "APP.TRADE.TAKE_PROFIT_MARKET", + iconUrl = null, + ), + ) } 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 903f15b8d..9b01bbfec 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/output/input/TradeInput.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/TradeInput.kt @@ -189,10 +189,10 @@ data class TradeInputOptions( needsReduceOnly = state.needsReduceOnly, needsPostOnly = state.needsPostOnly, needsBrackets = state.needsBrackets, - typeOptions = typeOptionsV4Array, - sideOptions = sideOptionsArray, + typeOptions = state.orderTypeOptions?.toIList() ?: iListOf(), + sideOptions = state.sideOptions?.toIList() ?: iListOf(), timeInForceOptions = state.timeInForceOptions?.toIList(), - goodTilUnitOptions = state.goodTilUnitOptions?.toIList() ?: goodTilUnitOptionsArray, + goodTilUnitOptions = state.goodTilUnitOptions?.toIList() ?: iListOf(), executionOptions = state.executionOptions?.toIList(), marginModeOptions = state.marginModeOptions?.toIList(), reduceOnlyTooltip = state.reduceOnlyTooltip, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt index 3da762951..17e173e3a 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt @@ -109,6 +109,8 @@ internal data class InternalTradeInputOptions( var needsReduceOnly: Boolean = false, var needsPostOnly: Boolean = false, var needsBrackets: Boolean = false, + var sideOptions: List? = null, + var orderTypeOptions: List? = null, var timeInForceOptions: List? = null, var executionOptions: List? = null, var marginModeOptions: List? = null, From f5f4dfa3b1f1774a3e70136e47ae289f5cd355a7 Mon Sep 17 00:00:00 2001 From: Rui Date: Tue, 13 Aug 2024 15:01:47 -0700 Subject: [PATCH 34/63] Add isOpen --- .../kotlin/exchange.dydx.abacus/calculator/MarginCalculator.kt | 3 +-- .../kotlin/exchange.dydx.abacus/output/input/TradeInput.kt | 3 +++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginCalculator.kt index 5035524ce..664bce025 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginCalculator.kt @@ -237,8 +237,7 @@ internal object MarginCalculator { val subaccount = it.value val openPositions = subaccount.openPositions val openOrders = subaccount.orders?.filter { order -> - val status = order.status - order.status == OrderStatus.Open || status == OrderStatus.Pending || status == OrderStatus.Untriggered || status == OrderStatus.PartiallyFilled + order.status.isOpen } val positionMarketIds = openPositions?.values?.mapNotNull { position -> 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 9b01bbfec..f6a969d8f 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/output/input/TradeInput.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/TradeInput.kt @@ -777,6 +777,9 @@ enum class OrderStatus(val rawValue: String) { // once an order is filled, canceled, or canceled with partial fill // there is no need to update status again get() = listOf(Filled, Canceled, PartiallyCanceled).contains(this) + + val isOpen: Boolean + get() = listOf(Open, Pending, Untriggered, PartiallyFilled).contains(this) } @JsExport From 545fe6b06a336cdbd14c7dab032c534301e13577 Mon Sep 17 00:00:00 2001 From: Rui Date: Tue, 13 Aug 2024 15:03:36 -0700 Subject: [PATCH 35/63] Cleanup --- .../exchange.dydx.abacus/state/internalstate/InternalState.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt index 17e173e3a..4827c6e7f 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt @@ -92,8 +92,8 @@ internal data class InternalTradeInputSummary( val filled: Boolean, val positionMargin: Double?, val positionLeverage: Double?, - val indexSlippage: Double? = null, - val feeRate: Double? = null, + val indexSlippage: Double?, + val feeRate: Double?, ) internal data class InternalTradeInputOptions( From 048a2dd71632205148e9d8001a87e5e19babf6f8 Mon Sep 17 00:00:00 2001 From: mobile-build-bot-git Date: Tue, 13 Aug 2024 22:04:50 +0000 Subject: [PATCH 36/63] Bump version --- build.gradle.kts | 2 +- v4_abacus.podspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 3ac7ed2d5..dfc8996e7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -52,7 +52,7 @@ allprojects { } group = "exchange.dydx.abacus" -version = "1.8.85" +version = "1.8.86" repositories { google() diff --git a/v4_abacus.podspec b/v4_abacus.podspec index f16d0e235..38af21d72 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.85' + spec.version = '1.8.86' spec.homepage = 'https://github.com/dydxprotocol/v4-abacus' spec.source = { :http=> ''} spec.authors = '' From 5cb9afba700f409e6b84674ab940bac671411749 Mon Sep 17 00:00:00 2001 From: Rui Date: Tue, 13 Aug 2024 15:07:02 -0700 Subject: [PATCH 37/63] Clean up --- .../exchange.dydx.abacus/calculator/MarginCalculator.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginCalculator.kt index 664bce025..372ccd940 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginCalculator.kt @@ -6,7 +6,6 @@ import exchange.dydx.abacus.output.PerpetualMarketType import exchange.dydx.abacus.output.account.Subaccount import exchange.dydx.abacus.output.account.SubaccountOrder import exchange.dydx.abacus.output.input.MarginMode -import exchange.dydx.abacus.output.input.OrderStatus import exchange.dydx.abacus.output.input.TradeInput import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.internalstate.InternalAccountState @@ -70,8 +69,7 @@ internal object MarginCalculator { ): SubaccountOrder? { val orders = account.groupedSubaccounts[subaccountNumber]?.orders return orders?.firstOrNull { - it.marketId == marketId && - it.status in listOf(OrderStatus.Open, OrderStatus.Pending, OrderStatus.Untriggered, OrderStatus.PartiallyFilled) + it.marketId == marketId && it.status.isOpen } } From b86eadc44129c4e3cbffc150d898beaa49616338 Mon Sep 17 00:00:00 2001 From: Rui Date: Tue, 13 Aug 2024 15:17:54 -0700 Subject: [PATCH 38/63] Lint --- .../TradeInputMarketOrderCalculator.kt | 24 ++--- .../TradeInput/TradeInputSummaryCalculator.kt | 5 -- .../input/TradeInputField+Actions.kt | 89 ++++++++++--------- 3 files changed, 57 insertions(+), 61 deletions(-) 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 80afb1554..59d863682 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 @@ -391,35 +391,35 @@ internal class TradeInputMarketOrderCalculator( /* Breaking naming rules a little bit to match the documentation above */ - @Suppress("LocalVariableName", "PropertyName") - val OR = oraclePrice + @Suppress("LocalVariableName", "PropertyName", "VariableNaming") + val _OR = oraclePrice - @Suppress("LocalVariableName", "PropertyName") + @Suppress("LocalVariableName", "PropertyName", "VariableNaming") val LV = leverage - @Suppress("LocalVariableName", "PropertyName") + @Suppress("LocalVariableName", "PropertyName", "VariableNaming") val OS: Double = if (isBuying) Numeric.double.POSITIVE else Numeric.double.NEGATIVE - @Suppress("LocalVariableName", "PropertyName") + @Suppress("LocalVariableName", "PropertyName", "VariableNaming") val FR = feeRate - @Suppress("LocalVariableName", "PropertyName") + @Suppress("LocalVariableName", "PropertyName", "VariableNaming") var AE = equity - @Suppress("LocalVariableName", "PropertyName") + @Suppress("LocalVariableName", "PropertyName", "VariableNaming") var SZ = positionSize ?: Numeric.double.ZERO orderbookLoop@ for (element in orderbook) { val entryPrice = element.price val entrySize = element.size if (entryPrice != Numeric.double.ZERO) { - @Suppress("LocalVariableName", "PropertyName") + @Suppress("LocalVariableName", "PropertyName", "VariableNaming") val MP = entryPrice - @Suppress("LocalVariableName", "PropertyName") - val X = ((LV * AE) - (SZ * OR)) / - (OR + (OS * LV * MP * FR) - (LV * (OR - MP))) + @Suppress("LocalVariableName", "PropertyName", "VariableNaming") + val X = ((LV * AE) - (SZ * _OR)) / + (_OR + (OS * LV * MP * FR) - (LV * (_OR - MP))) val desiredSize = X.abs() if (desiredSize < entrySize) { val rounded = this.rounded(sizeTotal, desiredSize, stepSize) @@ -439,7 +439,7 @@ internal class TradeInputMarketOrderCalculator( if (!isBuying) { signedSize *= exchange.dydx.abacus.utils.Numeric.double.NEGATIVE } - AE = AE + (signedSize * (OR - MP)) - (rounded * MP * FR) + AE = AE + (signedSize * (_OR - MP)) - (rounded * MP * FR) SZ += signedSize marketOrderOrderBook.add(matchingOrderbookEntry(element, rounded)) } 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 4fe01fccc..b2aa0b8ce 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 @@ -13,7 +13,6 @@ import exchange.dydx.abacus.output.FeeTier import exchange.dydx.abacus.output.input.MarginMode import exchange.dydx.abacus.output.input.OrderSide import exchange.dydx.abacus.output.input.OrderType -import exchange.dydx.abacus.output.input.TradeInputMarketOrder import exchange.dydx.abacus.state.internalstate.InternalMarketState import exchange.dydx.abacus.state.internalstate.InternalRewardsParamsState import exchange.dydx.abacus.state.internalstate.InternalSubaccountState @@ -331,10 +330,6 @@ internal class TradeInputSummaryCalculator { } } - private fun marketOrderWorstPrice(marketOrder: TradeInputMarketOrder): Double? { - return marketOrder.worstPrice - } - private fun slippage(price: Double?, oraclePrice: Double?, side: OrderSide?): Double? { return if (price != null && oraclePrice != null) { if (side == OrderSide.Buy) price - oraclePrice else oraclePrice - price diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputField+Actions.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputField+Actions.kt index c4257c16a..302c12e7e 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputField+Actions.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputField+Actions.kt @@ -18,17 +18,18 @@ import exchange.dydx.abacus.state.model.TradeInputField // // Returns the validation action for the trade input field +@Suppress("ExplicitItLambdaParameter") internal val TradeInputField.validTradeInputAction: ((InternalTradeInputState) -> Boolean)? get() = when (this) { TradeInputField.type, TradeInputField.side -> null - TradeInputField.size, TradeInputField.usdcSize, TradeInputField.leverage -> { it -> it.options.needsSize } - TradeInputField.limitPrice -> { it -> it.options.needsLimitPrice } - TradeInputField.triggerPrice -> { it -> it.options.needsTriggerPrice } - TradeInputField.trailingPercent -> { it -> it.options.needsTrailingPercent } - TradeInputField.targetLeverage -> { it -> it.options.needsTargetLeverage } - TradeInputField.goodTilDuration, TradeInputField.goodTilUnit -> { it -> it.options.needsGoodUntil } - TradeInputField.reduceOnly -> { it -> it.options.needsReduceOnly } - TradeInputField.postOnly -> { it -> it.options.needsPostOnly } + TradeInputField.size, TradeInputField.usdcSize, TradeInputField.leverage -> { state -> state.options.needsSize } + TradeInputField.limitPrice -> { state -> state.options.needsLimitPrice } + TradeInputField.triggerPrice -> { state -> state.options.needsTriggerPrice } + TradeInputField.trailingPercent -> { state -> state.options.needsTrailingPercent } + TradeInputField.targetLeverage -> { state -> state.options.needsTargetLeverage } + TradeInputField.goodTilDuration, TradeInputField.goodTilUnit -> { state -> state.options.needsGoodUntil } + TradeInputField.reduceOnly -> { state -> state.options.needsReduceOnly } + TradeInputField.postOnly -> { state -> state.options.needsPostOnly } TradeInputField.bracketsStopLossPrice, TradeInputField.bracketsStopLossPercent, TradeInputField.bracketsTakeProfitPrice, @@ -37,48 +38,48 @@ internal val TradeInputField.validTradeInputAction: ((InternalTradeInputState) - TradeInputField.bracketsGoodUntilUnit, TradeInputField.bracketsStopLossReduceOnly, TradeInputField.bracketsTakeProfitReduceOnly, - TradeInputField.bracketsExecution -> { it -> it.options.needsBrackets } - TradeInputField.timeInForceType -> { it -> it.options.timeInForceOptions != null } - TradeInputField.execution -> { it -> it.options.executionOptions != null } - TradeInputField.marginMode -> { it -> it.options.marginModeOptions != null } + TradeInputField.bracketsExecution -> { state -> state.options.needsBrackets } + TradeInputField.timeInForceType -> { state -> state.options.timeInForceOptions != null } + TradeInputField.execution -> { state -> state.options.executionOptions != null } + TradeInputField.marginMode -> { state -> state.options.marginModeOptions != null } TradeInputField.lastInput -> { it -> true } } // Returns the action to read value for the trade input field internal val TradeInputField.valueAction: ((InternalTradeInputState) -> Any?)? get() = when (this) { - TradeInputField.type -> { it -> it.type } - TradeInputField.side -> { it -> it.side } - - TradeInputField.marginMode -> { it -> it.marginMode } - TradeInputField.targetLeverage -> { it -> it.targetLeverage } - - TradeInputField.size -> { it -> it.size?.size } - TradeInputField.usdcSize -> { it -> it.size?.usdcSize } - TradeInputField.leverage -> { it -> it.size?.leverage } - - TradeInputField.lastInput -> { it -> it.size?.input } - TradeInputField.limitPrice -> { it -> it.price?.limitPrice } - TradeInputField.triggerPrice -> { it -> it.price?.triggerPrice } - TradeInputField.trailingPercent -> { it -> it.price?.trailingPercent } - - TradeInputField.timeInForceType -> { it -> it.timeInForce } - TradeInputField.goodTilDuration -> { it -> it.goodTil?.duration } - TradeInputField.goodTilUnit -> { it -> it.goodTil?.unit } - - TradeInputField.execution -> { it -> it.execution } - TradeInputField.reduceOnly -> { it -> it.reduceOnly } - TradeInputField.postOnly -> { it -> it.postOnly } - - TradeInputField.bracketsStopLossPrice -> { it -> it.bracket?.stopLoss?.triggerPrice } - TradeInputField.bracketsStopLossPercent -> { it -> it.bracket?.stopLoss?.percent } - TradeInputField.bracketsStopLossReduceOnly -> { it -> it.bracket?.stopLoss?.reduceOnly } - TradeInputField.bracketsTakeProfitPrice -> { it -> it.bracket?.takeProfit?.triggerPrice } - TradeInputField.bracketsTakeProfitPercent -> { it -> it.bracket?.takeProfit?.percent } - TradeInputField.bracketsTakeProfitReduceOnly -> { it -> it.bracket?.takeProfit?.reduceOnly } - TradeInputField.bracketsGoodUntilDuration -> { it -> it.bracket?.goodTil?.duration } - TradeInputField.bracketsGoodUntilUnit -> { it -> it.bracket?.goodTil?.unit } - TradeInputField.bracketsExecution -> { it -> it.bracket?.execution } + TradeInputField.type -> { state -> state.type } + TradeInputField.side -> { state -> state.side } + + TradeInputField.marginMode -> { state -> state.marginMode } + TradeInputField.targetLeverage -> { state -> state.targetLeverage } + + TradeInputField.size -> { state -> state.size?.size } + TradeInputField.usdcSize -> { state -> state.size?.usdcSize } + TradeInputField.leverage -> { state -> state.size?.leverage } + + TradeInputField.lastInput -> { state -> state.size?.input } + TradeInputField.limitPrice -> { state -> state.price?.limitPrice } + TradeInputField.triggerPrice -> { state -> state.price?.triggerPrice } + TradeInputField.trailingPercent -> { state -> state.price?.trailingPercent } + + TradeInputField.timeInForceType -> { state -> state.timeInForce } + TradeInputField.goodTilDuration -> { state -> state.goodTil?.duration } + TradeInputField.goodTilUnit -> { state -> state.goodTil?.unit } + + TradeInputField.execution -> { state -> state.execution } + TradeInputField.reduceOnly -> { state -> state.reduceOnly } + TradeInputField.postOnly -> { state -> state.postOnly } + + TradeInputField.bracketsStopLossPrice -> { state -> state.bracket?.stopLoss?.triggerPrice } + TradeInputField.bracketsStopLossPercent -> { state -> state.bracket?.stopLoss?.percent } + TradeInputField.bracketsStopLossReduceOnly -> { state -> state.bracket?.stopLoss?.reduceOnly } + TradeInputField.bracketsTakeProfitPrice -> { state -> state.bracket?.takeProfit?.triggerPrice } + TradeInputField.bracketsTakeProfitPercent -> { state -> state.bracket?.takeProfit?.percent } + TradeInputField.bracketsTakeProfitReduceOnly -> { state -> state.bracket?.takeProfit?.reduceOnly } + TradeInputField.bracketsGoodUntilDuration -> { state -> state.bracket?.goodTil?.duration } + TradeInputField.bracketsGoodUntilUnit -> { state -> state.bracket?.goodTil?.unit } + TradeInputField.bracketsExecution -> { state -> state.bracket?.execution } } // Returns the write action to update value for the trade input field From 3df5e4f66fb72db94e59b7cc977bb3f939e5db65 Mon Sep 17 00:00:00 2001 From: Rui Date: Wed, 14 Aug 2024 14:05:23 -0700 Subject: [PATCH 39/63] WIP --- .../calculator/V2/AccountTransformerV2.kt | 108 ++++++ .../calculator/V2/SubaccountTransformerV2.kt | 333 ++++++++++++++++++ .../markets/OrderbookEntryProcessor.kt | 2 +- 3 files changed, 442 insertions(+), 1 deletion(-) create mode 100644 src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/AccountTransformerV2.kt create mode 100644 src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/SubaccountTransformerV2.kt diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/AccountTransformerV2.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/AccountTransformerV2.kt new file mode 100644 index 000000000..c1b3aa7d0 --- /dev/null +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/AccountTransformerV2.kt @@ -0,0 +1,108 @@ +package exchange.dydx.abacus.calculator.v2 + +import exchange.dydx.abacus.calculator.CalculationPeriod +import exchange.dydx.abacus.calculator.MarginCalculator +import exchange.dydx.abacus.calculator.SubaccountTransformer +import exchange.dydx.abacus.output.input.MarginMode +import exchange.dydx.abacus.protocols.ParserProtocol +import exchange.dydx.abacus.state.internalstate.InternalAccountState +import exchange.dydx.abacus.state.internalstate.InternalMarketState +import exchange.dydx.abacus.state.internalstate.InternalTradeInputState +import exchange.dydx.abacus.utils.safeSet + +internal class AccountTransformerV2( + val parser: ParserProtocol, + private val subaccountTransformer: SubaccountTransformerV2 = SubaccountTransformerV2() +) { + fun applyTradeToAccount( + account: InternalAccountState, + subaccountNumber: Int, + trade: InternalTradeInputState, + market: InternalMarketState?, + period: CalculationPeriod, + ) { + val childSubaccountNumber = if (trade.marginMode == MarginMode.Isolated) { + val marketId = trade.marketId + if (marketId != null) { + MarginCalculator.getChildSubaccountNumberForIsolatedMarginTrade( + parser = parser, + subaccounts = account.subaccounts, + subaccountNumber = subaccountNumber, + marketId = marketId, + ) + } else { + null + } + } else { + subaccountNumber + } + + if (subaccountNumber == childSubaccountNumber) { + // CROSS + val subaccount = account.subaccounts[subaccountNumber] + subaccountTransformer.applyTradeToSubaccount( + subaccount = subaccount, + trade = trade, + market = market, + period = period, + ) + } else if (childSubaccountNumber != null) { + val childSubaccount = account.subaccounts[childSubaccountNumber] + + var transferAmountAppliedToParent = 0.0 + var transferAmountAppliedToChild = 0.0 + + val shouldTransferCollateralToChild = MarginCalculator.getShouldTransferInCollateral( + trade = trade, + subaccount = childSubaccount, + ) + val shouldTransferOutRemainingCollateralFromChild = + MarginCalculator.getShouldTransferOutRemainingCollateral( + parser, + subaccount = childSubaccount, + trade + ) + + if (shouldTransferCollateralToChild) { + val transferAmount = MarginCalculator.calculateIsolatedMarginTransferAmount( + parser, + trade, + market, + subaccount = childSubaccount + ) ?: 0.0 + transferAmountAppliedToParent = transferAmount * -1 + transferAmountAppliedToChild = transferAmount + } else if (shouldTransferOutRemainingCollateralFromChild) { + val remainingCollateral = + MarginCalculator.getEstimateRemainingCollateralAfterClosePosition( + parser, + subaccount = childSubaccount, + trade + ) ?: 0.0 + transferAmountAppliedToParent = remainingCollateral + } + + val modifiedParentSubaccount = subaccountTransformer.applyTransferToSubaccount( + subaccount, + transfer = transferAmountAppliedToParent, + parser, + period, + ) + modified.safeSet("subaccounts.$subaccountNumber", modifiedParentSubaccount) + + // when transfer out is true, post order position margin should be null + val modifiedChildSubaccount = subaccountTransformer.applyTradeToSubaccount( + childSubaccount, + trade, + market, + parser, + period, + transferAmountAppliedToChild, + isTransferOut = shouldTransferOutRemainingCollateralFromChild, + ) + modified.safeSet("subaccounts.$childSubaccountNumber", modifiedChildSubaccount) + + return modified + } + +} \ No newline at end of file diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/SubaccountTransformerV2.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/SubaccountTransformerV2.kt new file mode 100644 index 000000000..6c0bb1ad6 --- /dev/null +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/SubaccountTransformerV2.kt @@ -0,0 +1,333 @@ +package exchange.dydx.abacus.calculator.v2 + +import abs +import exchange.dydx.abacus.calculator.CalculationPeriod +import exchange.dydx.abacus.output.input.OrderSide +import exchange.dydx.abacus.protocols.ParserProtocol +import exchange.dydx.abacus.state.internalstate.InternalMarketState +import exchange.dydx.abacus.state.internalstate.InternalPerpetualPosition +import exchange.dydx.abacus.state.internalstate.InternalPositionCalculated +import exchange.dydx.abacus.state.internalstate.InternalSubaccountCalculated +import exchange.dydx.abacus.state.internalstate.InternalSubaccountState +import exchange.dydx.abacus.state.internalstate.InternalTradeInputState +import exchange.dydx.abacus.utils.Numeric +import exchange.dydx.abacus.utils.mutable +import exchange.dydx.abacus.utils.safeSet +import indexer.codegen.IndexerPerpetualPositionStatus +import kotlin.math.max +import kotlin.math.min + +private data class Delta( + val marketId: String? = null, + val size: Double? = null, + val price: Double? = null, + val usdcSize: Double? = null, + val fee: Double? = null, + val feeRate: Double? = null, + val reduceOnly: Boolean? = null, +) + +internal class SubaccountTransformerV2( + val parser: ParserProtocol +) { + fun applyTradeToSubaccount( + subaccount: InternalSubaccountState?, + trade: InternalTradeInputState, + market: InternalMarketState?, + period: CalculationPeriod, + transfer: Double? = null, + isTransferOut: Boolean? = false, + ) { + if (subaccount != null) { + // when isTransferOut is true, usdcSize is overwritten to 0 + val delta = deltaFromTrade( + trade = trade, + market = market, + transfer = transfer, + shouldTransferOut = isTransferOut, + ) + applyDeltaToSubaccount( + subaccount = subaccount, + delta = delta, + period = period, + hasTransfer = transfer != null + ) + } + } + + private fun deltaFromTrade( + trade: InternalTradeInputState, + market: InternalMarketState?, + transfer: Double? = null, + shouldTransferOut: Boolean? = false, + ): Delta? { + val marketId = trade.marketId ?: return null + val side = trade.side ?: return null + + val summary = trade.summary + + if (summary != null && summary.filled) { + val multiplier = if (side == OrderSide.Buy) Numeric.double.NEGATIVE else Numeric.double.POSITIVE + val originalPrice = summary.price + val price = if (market != null) { + executionPrice( + oraclePrice = market.perpetualMarket?.oraclePrice, + limitPrice = originalPrice, + isBuying = side == OrderSide.Buy, + ) + } else originalPrice + val size = (summary.size ?: Numeric.double.ZERO) * multiplier * Numeric.double.NEGATIVE + val usdcSize = (price ?: Numeric.double.ZERO) * ( + summary.size ?: Numeric.double.ZERO + ) * multiplier + (transfer ?: 0.0) + val fee = (summary.fee ?: Numeric.double.ZERO) * Numeric.double.NEGATIVE + val feeRate = summary.feeRate ?: Numeric.double.ZERO + + if (price != null && size != Numeric.double.ZERO) { + return Delta( + marketId = marketId, + size = size, + price = price, + usdcSize = if (shouldTransferOut == true) 0.0 else usdcSize, + fee = fee, + feeRate = feeRate, + reduceOnly = trade.reduceOnly, + ) + } + } + + return Delta( + marketId = marketId, + usdcSize = transfer, + ) + } + + private fun executionPrice( + oraclePrice: Double?, + limitPrice: Double?, + isBuying: Boolean, + ): Double? { + // use optimistic price by default + oraclePrice?.let { oraclePrice -> + limitPrice?.let { limitPrice -> + return if (isBuying) { + min(oraclePrice, limitPrice) + } else { + max(oraclePrice, limitPrice) + } + } + } + return limitPrice + } + + private fun applyDeltaToSubaccount( + subaccount: InternalSubaccountState, + delta: Delta?, + period: CalculationPeriod, + hasTransfer: Boolean = false, + ): InternalSubaccountState { + val deltaMarketId = delta?.marketId + val positions = subaccount.positions + + val marketPosition = positions?.get(deltaMarketId) + val modifiedDelta = if (delta != null) { + val positionSize = marketPosition?.calculated?.get(CalculationPeriod.current)?.size ?: Numeric.double.ZERO + transformDelta( + delta = delta, + positionSize = positionSize, + hasTransfer = hasTransfer, + ) + } else { + null + } + + if (positions != null) { + subaccount.positions = applyDeltaToPositions( + positions = positions, + delta = modifiedDelta, + period = period + ) + } + + val calculatedAtPeriod = subaccount.calculated[period] ?: InternalSubaccountCalculated() + val usdcSize = modifiedDelta?.usdcSize ?: Numeric.double.ZERO + if (delta != null && usdcSize != Numeric.double.ZERO) { + val fee = modifiedDelta?.fee ?: Numeric.double.ZERO + val quoteBalance = subaccount.calculated[CalculationPeriod.current]?.quoteBalance ?: Numeric.double.ZERO + calculatedAtPeriod.quoteBalance = quoteBalance + usdcSize + fee + } else { + calculatedAtPeriod.quoteBalance = null + } + subaccount.calculated[period] = calculatedAtPeriod + + return subaccount + } + + private fun transformDelta( + delta: Delta, + positionSize: Double, + hasTransfer: Boolean = false, + ): Delta { + val marketId = delta.marketId + if (delta.reduceOnly == true && !hasTransfer) { + val size = delta.size ?: Numeric.double.ZERO + val price = delta.price ?: Numeric.double.ZERO + val modifiedSize = + if (positionSize > Numeric.double.ZERO && size < Numeric.double.ZERO) { + maxOf(size, positionSize * Numeric.double.NEGATIVE) + } else if (positionSize < Numeric.double.ZERO && size > Numeric.double.ZERO) { + minOf(size, positionSize * Numeric.double.NEGATIVE) + } else { + Numeric.double.ZERO + } + val usdcSize = modifiedSize * price * Numeric.double.NEGATIVE + val feeRate = delta.feeRate ?: Numeric.double.ZERO + val fee = (usdcSize * feeRate).abs() * Numeric.double.NEGATIVE + return Delta( + marketId = marketId, + size = size, + price = price, + usdcSize = usdcSize, + fee = fee, + feeRate = feeRate, + reduceOnly = true, + ) + } + return delta + } + + private fun applyDeltaToPositions( + positions: Map, + delta: Delta?, + period: CalculationPeriod, + ): Map { + val modified = positions.mutable() + + val deltaMarketId = delta?.marketId + val size = delta?.size + val nullDelta = if (deltaMarketId != null) { + // Trade input + if (delta != null) { + if (size != null) { + Delta(size = 0.0) + } else { + Delta() + } + } else { + null + } + } else { + // Not a trade input. So we want the postOrder positions to be the same as the current positions + Delta(size = 0.0) + } + + val openPositions = modified.filterValues { + it.status == IndexerPerpetualPositionStatus.OPEN + } + for ((marketId, position) in openPositions) { + val currentSize = position.calculated[CalculationPeriod.current]?.size + if (marketId == deltaMarketId || currentSize != null) { + applyDeltaToPosition( + position = position, + delta = if (deltaMarketId == marketId) delta else nullDelta, + period = period, + ) + } + } + + if (openPositions[deltaMarketId] == null && deltaMarketId != null) { + // position didn't exists + val position = nullPosition(deltaMarketId) + val modifiedDelta = if (delta != null) { + transformDelta( + delta = delta, + positionSize = Numeric.double.ZERO, + ) + } else { + null + } + modified[deltaMarketId] = applyDeltaToPosition( + position = position, + delta = modifiedDelta, + period = period + ) + } + + return removeNullPositions( + positions = modified, + exceptMarketId = deltaMarketId + ) + } + + private fun removeNullPositions( + positions: Map, + exceptMarketId: String? + ): Map { + return positions.filterValues { position -> + val marketId = position.market + val current = position.calculated[CalculationPeriod.current]?.size ?: 0.0 + val postOrder = position.calculated[CalculationPeriod.post]?.size ?: 0.0 + (marketId != exceptMarketId) || (current != 0.0 || postOrder != 0.0) + } + } + + private fun applyDeltaToPosition( + position: InternalPerpetualPosition, + delta: Delta?, + period: CalculationPeriod, + ): InternalPerpetualPosition { + val deltaSize = delta?.size + val calculatedAtPeriod = position.calculated[period] ?: InternalPositionCalculated() + if (delta != null && deltaSize != null) { + val currentSize = position.calculated[CalculationPeriod.current]?.size ?: Numeric.double.ZERO + calculatedAtPeriod.size = currentSize + deltaSize + } else { + calculatedAtPeriod.size = null + } + position.calculated[period] = calculatedAtPeriod + return position + } + + private fun nullPosition(marketId: String): InternalPerpetualPosition { + return InternalPerpetualPosition( + market = marketId, + status = IndexerPerpetualPositionStatus.OPEN, + side = null, + size = 0.0, + maxSize = 0.0, + entryPrice = 0.0, + realizedPnl = 0.0, + createdAt = null, + createdAtHeight = null, + sumOpen = null, + sumClose = null, + netFunding = 0.0, + unrealizedPnl = 0.0, + closedAt = null, + exitPrice = null, + subaccountNumber = null, + resources = null, + calculated = mutableMapOf( + CalculationPeriod.current to InternalPositionCalculated( + valueTotal = 0.0, + notionalTotal = 0.0, + adjustedImf = null, + adjustedMmf = null, + initialRiskTotal = null, + maxLeverage = null, + unrealizedPnl = null, + unrealizedPnlPercent = null, + marginValue = null, + realizedPnlPercent = 0.0, + leverage = null, + size = 0.0, + liquidationPrice = null, + buyingPower = null + + ) + ) + + ) + } + +} diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/OrderbookEntryProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/OrderbookEntryProcessor.kt index c7a801a23..355d5625e 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/OrderbookEntryProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/OrderbookEntryProcessor.kt @@ -18,7 +18,7 @@ internal class OrderbookEntryProcessor(parser: ParserProtocol) : BaseProcessor(p payload: Map ): Map { val received = transform(existing, payload, orderbookEntryKeyMap) - received["offset"] = 0.toLong() + received["offset"] = 0.toLong()f return received } From c27e381f8c615d1f6ad812de41844e1a1964b9a2 Mon Sep 17 00:00:00 2001 From: Rui Date: Wed, 14 Aug 2024 20:30:46 -0700 Subject: [PATCH 40/63] AccountTransformer --- .../calculator/AccountTransformer.kt | 8 +- .../calculator/MarginCalculator.kt | 157 ++++++++++++++++-- .../calculator/V2/AccountTransformerV2.kt | 111 ++++++------- .../calculator/V2/SubaccountTransformerV2.kt | 56 ++++--- .../V2/TradeInput/TradeInputCalculatorV2.kt | 11 ++ .../markets/OrderbookEntryProcessor.kt | 2 +- .../payload/IsolatedMarginModeTests.kt | 10 +- 7 files changed, 248 insertions(+), 107 deletions(-) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/AccountTransformer.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/AccountTransformer.kt index ec6de5286..fcb01758e 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/AccountTransformer.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/AccountTransformer.kt @@ -51,15 +51,15 @@ class AccountTransformer() { var transferAmountAppliedToParent = 0.0 var transferAmountAppliedToChild = 0.0 - val shouldTransferCollateralToChild = MarginCalculator.getShouldTransferInCollateral(parser, subaccount = childSubaccount, trade) - val shouldTransferOutRemainingCollateralFromChild = MarginCalculator.getShouldTransferOutRemainingCollateral(parser, subaccount = childSubaccount, trade) + val shouldTransferCollateralToChild = MarginCalculator.getShouldTransferInCollateralDeprecated(parser, subaccount = childSubaccount, trade) + val shouldTransferOutRemainingCollateralFromChild = MarginCalculator.getShouldTransferOutRemainingCollateralDeprecated(parser, subaccount = childSubaccount, trade) if (shouldTransferCollateralToChild) { - val transferAmount = MarginCalculator.calculateIsolatedMarginTransferAmount(parser, trade, market, subaccount = childSubaccount) ?: 0.0 + val transferAmount = MarginCalculator.calculateIsolatedMarginTransferAmountDeprecated(parser, trade, market, subaccount = childSubaccount) ?: 0.0 transferAmountAppliedToParent = transferAmount * -1 transferAmountAppliedToChild = transferAmount } else if (shouldTransferOutRemainingCollateralFromChild) { - val remainingCollateral = MarginCalculator.getEstimateRemainingCollateralAfterClosePosition(parser, subaccount = childSubaccount, trade) ?: 0.0 + val remainingCollateral = MarginCalculator.getEstimateRemainingCollateralAfterClosePositionDeprecated(parser, subaccount = childSubaccount, trade) ?: 0.0 transferAmountAppliedToParent = remainingCollateral } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginCalculator.kt index 372ccd940..07c09f314 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginCalculator.kt @@ -6,6 +6,7 @@ import exchange.dydx.abacus.output.PerpetualMarketType import exchange.dydx.abacus.output.account.Subaccount import exchange.dydx.abacus.output.account.SubaccountOrder import exchange.dydx.abacus.output.input.MarginMode +import exchange.dydx.abacus.output.input.OrderSide import exchange.dydx.abacus.output.input.TradeInput import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.internalstate.InternalAccountState @@ -94,7 +95,16 @@ internal object MarginCalculator { return if (order != null) order.value as Map else null } - fun hasExistingOrder( + private fun hasExistingOrder( + subaccount: InternalSubaccountState?, + marketId: String? + ): Boolean { + return subaccount?.orders?.any { order -> + order.marketId == marketId && order.status.isOpen + } ?: false + } + + private fun hasExistingOrderDeprecated( parser: ParserProtocol, subaccount: Map?, marketId: String? @@ -439,11 +449,25 @@ internal object MarginCalculator { * @description Determine if collateral should be transferred into child subaccount for an isolated margin trade */ internal fun getShouldTransferInCollateral( + subaccount: InternalSubaccountState?, + tradeInput: InternalTradeInputState?, + ): Boolean { + val isIncreasingPositionSize = getIsIncreasingPositionSize(subaccount, tradeInput) + val isIsolatedMarginOrder = tradeInput?.marginMode == MarginMode.Isolated + val isReduceOnly = tradeInput?.reduceOnly ?: false + + return isIncreasingPositionSize && isIsolatedMarginOrder && !isReduceOnly + } + + /** + * @description Determine if collateral should be transferred into child subaccount for an isolated margin trade + */ + internal fun getShouldTransferInCollateralDeprecated( parser: ParserProtocol, subaccount: Map?, tradeInput: Map?, ): Boolean { - val isIncreasingPositionSize = getIsIncreasingPositionSize(parser, subaccount, tradeInput) + val isIncreasingPositionSize = getIsIncreasingPositionSizeDeprecated(parser, subaccount, tradeInput) val isIsolatedMarginOrder = parser.asString(tradeInput?.get("marginMode")) == "ISOLATED" val isReduceOnly = parser.asBool(tradeInput?.get("reduceOnly")) ?: false @@ -467,20 +491,45 @@ internal object MarginCalculator { * @description Determine if collateral should be transferred out of child subaccount for an isolated margin trade */ internal fun getShouldTransferOutRemainingCollateral( + subaccount: InternalSubaccountState?, + tradeInput: InternalTradeInputState?, + ): Boolean { + val isPositionFullyClosed = getIsPositionFullyClosed(subaccount, tradeInput) + val isIsolatedMarginOrder = tradeInput?.marginMode == MarginMode.Isolated + val hasOpenOrder = tradeInput?.marketId?.let { marketId -> + hasExistingOrder(subaccount, marketId) + } ?: false + + return isPositionFullyClosed && isIsolatedMarginOrder && !hasOpenOrder + } + + /** + * @description Determine if collateral should be transferred out of child subaccount for an isolated margin trade + */ + internal fun getShouldTransferOutRemainingCollateralDeprecated( parser: ParserProtocol, subaccount: Map?, tradeInput: Map?, ): Boolean { - val isPositionFullyClosed = getIsPositionFullyClosed(parser, subaccount, tradeInput) + val isPositionFullyClosed = getIsPositionFullyClosedDeprecated(parser, subaccount, tradeInput) val isIsolatedMarginOrder = parser.asString(tradeInput?.get("marginMode")) == "ISOLATED" val hasOpenOrder = parser.asString(tradeInput?.get("marketId"))?.let { marketId -> - hasExistingOrder(parser, subaccount, marketId) + hasExistingOrderDeprecated(parser, subaccount, marketId) } ?: false return isPositionFullyClosed && isIsolatedMarginOrder && !hasOpenOrder } internal fun getEstimateRemainingCollateralAfterClosePosition( + subaccount: InternalSubaccountState?, + tradeInput: InternalTradeInputState?, + ): Double? { + val quoteBalance = subaccount?.calculated?.get(CalculationPeriod.current)?.quoteBalance ?: return null + val total = tradeInput?.summary?.total ?: return null + return quoteBalance + total + } + + internal fun getEstimateRemainingCollateralAfterClosePositionDeprecated( parser: ParserProtocol, subaccount: Map?, tradeInput: Map?, @@ -492,6 +541,20 @@ internal object MarginCalculator { } private fun getIsPositionFullyClosed( + subaccount: InternalSubaccountState?, + tradeInput: InternalTradeInputState?, + ): Boolean { + return tradeInput?.marketId?.let { marketId -> + val position = subaccount?.openPositions?.get(marketId) + val currentSize = position?.calculated?.get(CalculationPeriod.current)?.size ?: 0.0 + val postOrderSize = getPositionPostOrderSizeFromTrade(tradeInput, currentSize) + val isReduceOnly = tradeInput.reduceOnly + val hasFlippedSide = currentSize * postOrderSize < 0 + return postOrderSize == 0.0 || (isReduceOnly && hasFlippedSide) + } ?: false + } + + private fun getIsPositionFullyClosedDeprecated( parser: ParserProtocol, subaccount: Map?, tradeInput: Map?, @@ -499,7 +562,7 @@ internal object MarginCalculator { return parser.asString(tradeInput?.get("marketId"))?.let { marketId -> val position = parser.asNativeMap(parser.value(subaccount, "openPositions.$marketId")) val currentSize = parser.asDouble(parser.value(position, "size.current")) ?: 0.0 - val postOrderSize = tradeInput?.let { getPositionPostOrderSizeFromTrade(parser, tradeInput, currentSize) } ?: 0.0 + val postOrderSize = tradeInput?.let { getPositionPostOrderSizeFromTradeDeprecated(parser, tradeInput, currentSize) } ?: 0.0 val isReduceOnly = parser.asBool(tradeInput?.get("reduceOnly")) ?: false val hasFlippedSide = currentSize * postOrderSize < 0 return postOrderSize == 0.0 || (isReduceOnly && hasFlippedSide) @@ -507,11 +570,18 @@ internal object MarginCalculator { } private fun getIsIncreasingPositionSize( + subaccount: InternalSubaccountState?, + tradeInput: InternalTradeInputState?, + ): Boolean { + return getPositionSizeDifference(subaccount, tradeInput)?.let { it > 0 } ?: true + } + + private fun getIsIncreasingPositionSizeDeprecated( parser: ParserProtocol, subaccount: Map?, tradeInput: Map?, ): Boolean { - return getPositionSizeDifference(parser, subaccount, tradeInput)?.let { it > 0 } ?: true + return getPositionSizeDifferenceDeprecated(parser, subaccount, tradeInput)?.let { it > 0 } ?: true } private fun getIsIncreasingPositionSize( @@ -522,6 +592,17 @@ internal object MarginCalculator { } private fun getPositionSizeDifference( + subaccount: InternalSubaccountState?, + tradeInput: InternalTradeInputState?, + ): Double? { + val marketId = tradeInput?.marketId ?: return null + val position = subaccount?.openPositions?.get(marketId) + val currentSize = position?.calculated?.get(CalculationPeriod.current)?.size ?: 0.0 + val postOrderSize = getPositionPostOrderSizeFromTrade(tradeInput, currentSize) + return postOrderSize.abs() - currentSize.abs() + } + + private fun getPositionSizeDifferenceDeprecated( parser: ParserProtocol, subaccount: Map?, tradeInput: Map?, @@ -529,7 +610,7 @@ internal object MarginCalculator { return parser.asString(tradeInput?.get("marketId"))?.let { marketId -> val position = parser.asNativeMap(parser.value(subaccount, "openPositions.$marketId")) val currentSize = parser.asDouble(parser.value(position, "size.current")) ?: 0.0 - val postOrderSize = tradeInput?.let { getPositionPostOrderSizeFromTrade(parser, tradeInput, currentSize) } ?: 0.0 + val postOrderSize = tradeInput?.let { getPositionPostOrderSizeFromTradeDeprecated(parser, tradeInput, currentSize) } ?: 0.0 return postOrderSize.abs() - currentSize.abs() } } @@ -539,6 +620,24 @@ internal object MarginCalculator { * We need this estimate before the trade delta is applied to the position, as position post-order size may not be updated yet. */ private fun getPositionPostOrderSizeFromTrade( + trade: InternalTradeInputState, + currentPositionSize: Double, + ): Double { + val tradeSize = trade.summary?.takeIf { + trade.marketId != null && trade.side != null && it.filled + }?.let { summary -> + val multiplier = if (trade.side == OrderSide.Buy) Numeric.double.POSITIVE else Numeric.double.NEGATIVE + (summary.size ?: Numeric.double.ZERO) * multiplier + } ?: 0.0 + + return currentPositionSize + tradeSize + } + + /** + * @description Helper to determine post-order position size from current trade input instead of position.size.postOrder + * We need this estimate before the trade delta is applied to the position, as position post-order size may not be updated yet. + */ + private fun getPositionPostOrderSizeFromTradeDeprecated( parser: ParserProtocol, trade: Map, currentPositionSize: Double, @@ -572,6 +671,34 @@ internal object MarginCalculator { * Max leverage is capped at 98% of the the market's max leverage and takes the oraclePrice into account in order to pass collateral checks. */ internal fun calculateIsolatedMarginTransferAmount( + trade: InternalTradeInputState, + market: InternalMarketState?, + subaccount: InternalSubaccountState? + ): Double? { + val targetLeverage = trade.targetLeverage ?: 1.0 + val side = trade.side ?: return null + val oraclePrice = market?.perpetualMarket?.oraclePrice ?: return null + val price = trade.summary?.price ?: return null + val initialMarginFraction = market.perpetualMarket?.configs?.initialMarginFraction ?: 0.0 + val effectiveImf = market.perpetualMarket?.configs?.effectiveInitialMarginFraction ?: 0.0 + val positionSizeDifference = getPositionSizeDifference(subaccount, trade) ?: return null + + return calculateIsolatedMarginTransferAmountFromValues( + targetLeverage = targetLeverage, + side = side.rawValue, + oraclePrice = oraclePrice, + price = price, + initialMarginFraction = initialMarginFraction, + effectiveImf = effectiveImf, + positionSizeDifference = positionSizeDifference, + ) + } + + /** + * @description Calculate the amount of collateral to transfer for an isolated margin trade. + * Max leverage is capped at 98% of the the market's max leverage and takes the oraclePrice into account in order to pass collateral checks. + */ + internal fun calculateIsolatedMarginTransferAmountDeprecated( parser: ParserProtocol, trade: Map, market: Map?, @@ -583,16 +710,16 @@ internal object MarginCalculator { val price = parser.asDouble(parser.value(trade, "summary.price")) ?: return null val initialMarginFraction = parser.asDouble(parser.value(market, "configs.initialMarginFraction")) ?: 0.0 val effectiveImf = parser.asDouble(parser.value(market, "configs.effectiveInitialMarginFraction")) ?: 0.0 - val positionSizeDifference = getPositionSizeDifference(parser, subaccount, trade) ?: return null + val positionSizeDifference = getPositionSizeDifferenceDeprecated(parser, subaccount, trade) ?: return null return calculateIsolatedMarginTransferAmountFromValues( - targetLeverage, - side, - oraclePrice, - price, - initialMarginFraction, - effectiveImf, - positionSizeDifference, + targetLeverage = targetLeverage, + side = side, + oraclePrice = oraclePrice, + price = price, + initialMarginFraction = initialMarginFraction, + effectiveImf = effectiveImf, + positionSizeDifference = positionSizeDifference, ) } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/AccountTransformerV2.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/AccountTransformerV2.kt index c1b3aa7d0..c21380c14 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/AccountTransformerV2.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/AccountTransformerV2.kt @@ -2,17 +2,16 @@ package exchange.dydx.abacus.calculator.v2 import exchange.dydx.abacus.calculator.CalculationPeriod import exchange.dydx.abacus.calculator.MarginCalculator -import exchange.dydx.abacus.calculator.SubaccountTransformer import exchange.dydx.abacus.output.input.MarginMode import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.internalstate.InternalAccountState import exchange.dydx.abacus.state.internalstate.InternalMarketState +import exchange.dydx.abacus.state.internalstate.InternalSubaccountState import exchange.dydx.abacus.state.internalstate.InternalTradeInputState -import exchange.dydx.abacus.utils.safeSet internal class AccountTransformerV2( val parser: ParserProtocol, - private val subaccountTransformer: SubaccountTransformerV2 = SubaccountTransformerV2() + private val subaccountTransformer: SubaccountTransformerV2 = SubaccountTransformerV2(parser) ) { fun applyTradeToAccount( account: InternalAccountState, @@ -20,7 +19,7 @@ internal class AccountTransformerV2( trade: InternalTradeInputState, market: InternalMarketState?, period: CalculationPeriod, - ) { + ) { val childSubaccountNumber = if (trade.marginMode == MarginMode.Isolated) { val marketId = trade.marketId if (marketId != null) { @@ -37,72 +36,66 @@ internal class AccountTransformerV2( subaccountNumber } - if (subaccountNumber == childSubaccountNumber) { - // CROSS - val subaccount = account.subaccounts[subaccountNumber] - subaccountTransformer.applyTradeToSubaccount( + val subaccount = account.subaccounts[subaccountNumber] ?: InternalSubaccountState( + subaccountNumber = subaccountNumber, + ) + account.subaccounts[subaccountNumber] = subaccount + + if (subaccountNumber == childSubaccountNumber) { + // CROSS + subaccountTransformer.applyTradeToSubaccount( subaccount = subaccount, trade = trade, market = market, period = period, ) - } else if (childSubaccountNumber != null) { - val childSubaccount = account.subaccounts[childSubaccountNumber] + } else if (childSubaccountNumber != null) { + val childSubaccount = account.subaccounts[childSubaccountNumber] - var transferAmountAppliedToParent = 0.0 - var transferAmountAppliedToChild = 0.0 + var transferAmountAppliedToParent = 0.0 + var transferAmountAppliedToChild = 0.0 - val shouldTransferCollateralToChild = MarginCalculator.getShouldTransferInCollateral( - trade = trade, - subaccount = childSubaccount, - ) - val shouldTransferOutRemainingCollateralFromChild = - MarginCalculator.getShouldTransferOutRemainingCollateral( - parser, + val shouldTransferCollateralToChild = MarginCalculator.getShouldTransferInCollateral( subaccount = childSubaccount, - trade + tradeInput = trade, ) + val shouldTransferOutRemainingCollateralFromChild = + MarginCalculator.getShouldTransferOutRemainingCollateral( + subaccount = childSubaccount, + tradeInput = trade, + ) - if (shouldTransferCollateralToChild) { - val transferAmount = MarginCalculator.calculateIsolatedMarginTransferAmount( - parser, - trade, - market, - subaccount = childSubaccount - ) ?: 0.0 - transferAmountAppliedToParent = transferAmount * -1 - transferAmountAppliedToChild = transferAmount - } else if (shouldTransferOutRemainingCollateralFromChild) { - val remainingCollateral = - MarginCalculator.getEstimateRemainingCollateralAfterClosePosition( - parser, + if (shouldTransferCollateralToChild) { + val transferAmount = MarginCalculator.calculateIsolatedMarginTransferAmount( + trade = trade, + market = market, subaccount = childSubaccount, - trade ) ?: 0.0 - transferAmountAppliedToParent = remainingCollateral - } - - val modifiedParentSubaccount = subaccountTransformer.applyTransferToSubaccount( - subaccount, - transfer = transferAmountAppliedToParent, - parser, - period, - ) - modified.safeSet("subaccounts.$subaccountNumber", modifiedParentSubaccount) - - // when transfer out is true, post order position margin should be null - val modifiedChildSubaccount = subaccountTransformer.applyTradeToSubaccount( - childSubaccount, - trade, - market, - parser, - period, - transferAmountAppliedToChild, - isTransferOut = shouldTransferOutRemainingCollateralFromChild, - ) - modified.safeSet("subaccounts.$childSubaccountNumber", modifiedChildSubaccount) + transferAmountAppliedToParent = transferAmount * -1 + transferAmountAppliedToChild = transferAmount + } else if (shouldTransferOutRemainingCollateralFromChild) { + val remainingCollateral = + MarginCalculator.getEstimateRemainingCollateralAfterClosePosition( + subaccount = childSubaccount, + tradeInput = trade, + ) ?: 0.0 + transferAmountAppliedToParent = remainingCollateral + } + subaccountTransformer.applyTransferToSubaccount( + subaccount = subaccount, + transfer = transferAmountAppliedToParent, + period = period, + ) - return modified + // when transfer out is true, post order position margin should be null + subaccountTransformer.applyTradeToSubaccount( + subaccount = childSubaccount, + trade = trade, + market = market, + period = period, + transfer = transferAmountAppliedToChild, + isTransferOut = shouldTransferOutRemainingCollateralFromChild, + ) + } } - -} \ No newline at end of file +} diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/SubaccountTransformerV2.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/SubaccountTransformerV2.kt index 6c0bb1ad6..4a62f6b47 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/SubaccountTransformerV2.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/SubaccountTransformerV2.kt @@ -12,7 +12,6 @@ import exchange.dydx.abacus.state.internalstate.InternalSubaccountState import exchange.dydx.abacus.state.internalstate.InternalTradeInputState import exchange.dydx.abacus.utils.Numeric import exchange.dydx.abacus.utils.mutable -import exchange.dydx.abacus.utils.safeSet import indexer.codegen.IndexerPerpetualPositionStatus import kotlin.math.max import kotlin.math.min @@ -50,11 +49,23 @@ internal class SubaccountTransformerV2( subaccount = subaccount, delta = delta, period = period, - hasTransfer = transfer != null + hasTransfer = transfer != null, ) } } + fun applyTransferToSubaccount( + subaccount: InternalSubaccountState, + transfer: Double, + period: CalculationPeriod, + ) { + applyDeltaToSubaccount( + subaccount = subaccount, + delta = Delta(usdcSize = transfer), + period = period, + ) + } + private fun deltaFromTrade( trade: InternalTradeInputState, market: InternalMarketState?, @@ -67,19 +78,21 @@ internal class SubaccountTransformerV2( val summary = trade.summary if (summary != null && summary.filled) { - val multiplier = if (side == OrderSide.Buy) Numeric.double.NEGATIVE else Numeric.double.POSITIVE - val originalPrice = summary.price + val multiplier = if (side == OrderSide.Buy) Numeric.double.NEGATIVE else Numeric.double.POSITIVE + val originalPrice = summary.price val price = if (market != null) { executionPrice( oraclePrice = market.perpetualMarket?.oraclePrice, limitPrice = originalPrice, isBuying = side == OrderSide.Buy, ) - } else originalPrice + } else { + originalPrice + } val size = (summary.size ?: Numeric.double.ZERO) * multiplier * Numeric.double.NEGATIVE val usdcSize = (price ?: Numeric.double.ZERO) * ( - summary.size ?: Numeric.double.ZERO - ) * multiplier + (transfer ?: 0.0) + summary.size ?: Numeric.double.ZERO + ) * multiplier + (transfer ?: 0.0) val fee = (summary.fee ?: Numeric.double.ZERO) * Numeric.double.NEGATIVE val feeRate = summary.feeRate ?: Numeric.double.ZERO @@ -96,7 +109,7 @@ internal class SubaccountTransformerV2( } } - return Delta( + return Delta( marketId = marketId, usdcSize = transfer, ) @@ -125,7 +138,7 @@ internal class SubaccountTransformerV2( delta: Delta?, period: CalculationPeriod, hasTransfer: Boolean = false, - ): InternalSubaccountState { + ) { val deltaMarketId = delta?.marketId val positions = subaccount.positions @@ -145,22 +158,20 @@ internal class SubaccountTransformerV2( subaccount.positions = applyDeltaToPositions( positions = positions, delta = modifiedDelta, - period = period + period = period, ) } val calculatedAtPeriod = subaccount.calculated[period] ?: InternalSubaccountCalculated() - val usdcSize = modifiedDelta?.usdcSize ?: Numeric.double.ZERO + val usdcSize = modifiedDelta?.usdcSize ?: Numeric.double.ZERO if (delta != null && usdcSize != Numeric.double.ZERO) { val fee = modifiedDelta?.fee ?: Numeric.double.ZERO val quoteBalance = subaccount.calculated[CalculationPeriod.current]?.quoteBalance ?: Numeric.double.ZERO - calculatedAtPeriod.quoteBalance = quoteBalance + usdcSize + fee + calculatedAtPeriod.quoteBalance = quoteBalance + usdcSize + fee } else { calculatedAtPeriod.quoteBalance = null } subaccount.calculated[period] = calculatedAtPeriod - - return subaccount } private fun transformDelta( @@ -169,7 +180,7 @@ internal class SubaccountTransformerV2( hasTransfer: Boolean = false, ): Delta { val marketId = delta.marketId - if (delta.reduceOnly == true && !hasTransfer) { + if (delta.reduceOnly == true && !hasTransfer) { val size = delta.size ?: Numeric.double.ZERO val price = delta.price ?: Numeric.double.ZERO val modifiedSize = @@ -241,7 +252,7 @@ internal class SubaccountTransformerV2( val modifiedDelta = if (delta != null) { transformDelta( delta = delta, - positionSize = Numeric.double.ZERO, + positionSize = Numeric.double.ZERO, ) } else { null @@ -249,13 +260,13 @@ internal class SubaccountTransformerV2( modified[deltaMarketId] = applyDeltaToPosition( position = position, delta = modifiedDelta, - period = period + period = period, ) } return removeNullPositions( positions = modified, - exceptMarketId = deltaMarketId + exceptMarketId = deltaMarketId, ) } @@ -279,7 +290,7 @@ internal class SubaccountTransformerV2( val deltaSize = delta?.size val calculatedAtPeriod = position.calculated[period] ?: InternalPositionCalculated() if (delta != null && deltaSize != null) { - val currentSize = position.calculated[CalculationPeriod.current]?.size ?: Numeric.double.ZERO + val currentSize = position.calculated[CalculationPeriod.current]?.size ?: Numeric.double.ZERO calculatedAtPeriod.size = currentSize + deltaSize } else { calculatedAtPeriod.size = null @@ -322,12 +333,11 @@ internal class SubaccountTransformerV2( leverage = null, size = 0.0, liquidationPrice = null, - buyingPower = null + buyingPower = null, - ) - ) + ), + ), ) } - } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputCalculatorV2.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputCalculatorV2.kt index b4a3a5ad3..13687fb97 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputCalculatorV2.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputCalculatorV2.kt @@ -1,6 +1,8 @@ package exchange.dydx.abacus.calculator.v2.tradeinput +import exchange.dydx.abacus.calculator.CalculationPeriod import exchange.dydx.abacus.calculator.TradeCalculation +import exchange.dydx.abacus.calculator.v2.AccountTransformerV2 import exchange.dydx.abacus.output.FeeTier import exchange.dydx.abacus.output.input.OrderType import exchange.dydx.abacus.protocols.ParserProtocol @@ -22,6 +24,7 @@ internal class TradeInputCalculatorV2( private val nonMarketOrderCalculator: TradeInputNonMarketOrderCalculator = TradeInputNonMarketOrderCalculator(), private val optionsCalculator: TradeInputOptionsCalculator = TradeInputOptionsCalculator(parser), private val summaryCalculator: TradeInputSummaryCalculator = TradeInputSummaryCalculator(), + private val accountTransformer: AccountTransformerV2 = AccountTransformerV2(parser), ) { fun calculate( trade: InternalTradeInputState, @@ -86,6 +89,14 @@ internal class TradeInputCalculatorV2( feeTiers = configs.feeTiers, ) + accountTransformer.applyTradeToAccount( + account = account, + subaccountNumber = subaccountNumber, + trade = trade, + market = markets[trade.marketId], + CalculationPeriod.post, + ) + return trade } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/OrderbookEntryProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/OrderbookEntryProcessor.kt index 355d5625e..c7a801a23 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/OrderbookEntryProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/OrderbookEntryProcessor.kt @@ -18,7 +18,7 @@ internal class OrderbookEntryProcessor(parser: ParserProtocol) : BaseProcessor(p payload: Map ): Map { val received = transform(existing, payload, orderbookEntryKeyMap) - received["offset"] = 0.toLong()f + received["offset"] = 0.toLong() return received } diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/payload/IsolatedMarginModeTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/payload/IsolatedMarginModeTests.kt index 09da93775..33f3a654c 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/payload/IsolatedMarginModeTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/payload/IsolatedMarginModeTests.kt @@ -752,7 +752,7 @@ class IsolatedMarginModeTests : V4BaseTests(true) { fun testGetShouldTransferCollateral() { assertTrue( "Should result in a transfer", - MarginCalculator.getShouldTransferInCollateral( + MarginCalculator.getShouldTransferInCollateralDeprecated( parser, subaccount = mapOf( "openPositions" to mapOf( @@ -780,7 +780,7 @@ class IsolatedMarginModeTests : V4BaseTests(true) { // If reduce only is true, should not transfer in assertEquals( false, - MarginCalculator.getShouldTransferInCollateral( + MarginCalculator.getShouldTransferInCollateralDeprecated( parser, subaccount = mapOf( "openPositions" to mapOf( @@ -808,7 +808,7 @@ class IsolatedMarginModeTests : V4BaseTests(true) { // If full close + no open orders, should transfer out assertEquals( true, - MarginCalculator.getShouldTransferOutRemainingCollateral( + MarginCalculator.getShouldTransferOutRemainingCollateralDeprecated( parser, subaccount = mapOf( "openPositions" to mapOf( @@ -836,7 +836,7 @@ class IsolatedMarginModeTests : V4BaseTests(true) { // If postOrder is less than current, should not transfer in assertEquals( false, - MarginCalculator.getShouldTransferInCollateral( + MarginCalculator.getShouldTransferInCollateralDeprecated( parser, subaccount = mapOf( "openPositions" to mapOf( @@ -864,7 +864,7 @@ class IsolatedMarginModeTests : V4BaseTests(true) { // If reducing position to full close but has open orders, should not transfer out assertEquals( false, - MarginCalculator.getShouldTransferOutRemainingCollateral( + MarginCalculator.getShouldTransferOutRemainingCollateralDeprecated( parser, subaccount = mapOf( "openPositions" to mapOf( From af9b5203d06f155d72bcd200326c983fd6208f63 Mon Sep 17 00:00:00 2001 From: Rui Date: Thu, 15 Aug 2024 09:51:24 -0700 Subject: [PATCH 41/63] Trade receipts --- .../calculator/MarginCalculator.kt | 2 +- .../calculator/ReceiptCalculator.kt | 114 ++++++++++++++++++ .../V2/TradeInput/TradeInputCalculatorV2.kt | 8 +- .../output/input/Input.kt | 20 ++- .../output/input/ReceiptLine.kt | 2 +- .../state/internalstate/InternalState.kt | 2 + .../state/model/TradingStateMachine.kt | 110 +++++++++++------ 7 files changed, 214 insertions(+), 44 deletions(-) create mode 100644 src/commonMain/kotlin/exchange.dydx.abacus/calculator/ReceiptCalculator.kt diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginCalculator.kt index 07c09f314..5b3a43106 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginCalculator.kt @@ -205,7 +205,7 @@ internal object MarginCalculator { return if (existingMarginMode != null) { false } else if (marketId != null) { - findMarketMarginMode(market?.perpetualMarket) == MarginMode.Cross + findMarketMarginMode(market.perpetualMarket) == MarginMode.Cross } else { true } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/ReceiptCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/ReceiptCalculator.kt new file mode 100644 index 000000000..b0900f2d5 --- /dev/null +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/ReceiptCalculator.kt @@ -0,0 +1,114 @@ +package exchange.dydx.abacus.calculator + +import exchange.dydx.abacus.output.input.InputType +import exchange.dydx.abacus.output.input.OrderType +import exchange.dydx.abacus.output.input.ReceiptLine +import exchange.dydx.abacus.state.internalstate.InternalInputState + +internal class ReceiptCalculator { + fun calculate( + input: InternalInputState, + ): InternalInputState { + val receiptLines: List? = + when (input.currentType) { + InputType.TRADE -> { + val trade = input.trade + when (trade.type) { + OrderType.Market, OrderType.StopMarket, OrderType.TakeProfitMarket, OrderType.TrailingStop -> { + listOf( + ReceiptLine.ExpectedPrice, + ReceiptLine.LiquidationPrice, + ReceiptLine.PositionMargin, + ReceiptLine.PositionLeverage, + ReceiptLine.Fee, + ReceiptLine.Reward, + ) + } + + else -> { + listOf( + ReceiptLine.LiquidationPrice, + ReceiptLine.PositionMargin, + ReceiptLine.PositionLeverage, + ReceiptLine.Fee, + ReceiptLine.Reward, + ) + } + } + } + + InputType.CLOSE_POSITION -> { + listOf( + ReceiptLine.BuyingPower, + ReceiptLine.MarginUsage, + ReceiptLine.ExpectedPrice, + ReceiptLine.Fee, + ReceiptLine.Reward, + ) + } + + InputType.TRANSFER -> { + null // TODO when working with transfer + /* + val transfer = parser.asNativeMap(input["transfer"]) ?: return null + val type = parser.asString(transfer["type"]) ?: return null + return when (type) { + "DEPOSIT", "WITHDRAWAL" -> { + if (StatsigConfig.useSkip) { + listOf( + ReceiptLine.Equity.rawValue, + ReceiptLine.BuyingPower.rawValue, + ReceiptLine.BridgeFee.rawValue, + // add these back when supported by Skip +// ReceiptLine.ExchangeRate.rawValue, +// ReceiptLine.ExchangeReceived.rawValue, +// ReceiptLine.Fee.rawValue, + ReceiptLine.Slippage.rawValue, + ReceiptLine.TransferRouteEstimatedDuration.rawValue, + ) + } else { + listOf( + ReceiptLine.Equity.rawValue, + ReceiptLine.BuyingPower.rawValue, + ReceiptLine.ExchangeRate.rawValue, + ReceiptLine.ExchangeReceived.rawValue, + ReceiptLine.Fee.rawValue, +// ReceiptLine.BridgeFee.rawValue, + ReceiptLine.Slippage.rawValue, + ReceiptLine.TransferRouteEstimatedDuration.rawValue, + ) + } + } + + "TRANSFER_OUT" -> { + listOf( + ReceiptLine.Equity.rawValue, + ReceiptLine.MarginUsage.rawValue, + ReceiptLine.Fee.rawValue, + ) + } + + else -> { + listOf() + } + } + */ + } + + InputType.ADJUST_ISOLATED_MARGIN -> { + listOf( + ReceiptLine.CrossFreeCollateral, + ReceiptLine.CrossMarginUsage, + ReceiptLine.PositionLeverage, + ReceiptLine.PositionMargin, + ReceiptLine.LiquidationPrice, + ) + } + + else -> null + } + + input.receiptLines = receiptLines + return input + } +} diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputCalculatorV2.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputCalculatorV2.kt index 13687fb97..5378e7425 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputCalculatorV2.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputCalculatorV2.kt @@ -4,6 +4,7 @@ import exchange.dydx.abacus.calculator.CalculationPeriod import exchange.dydx.abacus.calculator.TradeCalculation import exchange.dydx.abacus.calculator.v2.AccountTransformerV2 import exchange.dydx.abacus.output.FeeTier +import exchange.dydx.abacus.output.input.MarginMode import exchange.dydx.abacus.output.input.OrderType import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.internalstate.InternalAccountState @@ -36,8 +37,10 @@ internal class TradeInputCalculatorV2( input: String?, ): InternalTradeInputState { val account = wallet.account + + val crossMarginSubaccount = account.subaccounts[subaccountNumber] val subaccount = - account.groupedSubaccounts[subaccountNumber] ?: account.subaccounts[subaccountNumber] + account.groupedSubaccounts[subaccountNumber] ?: crossMarginSubaccount val user = wallet.user val markets = marketSummary.markets @@ -56,7 +59,7 @@ internal class TradeInputCalculatorV2( marketOrderCalculator.calculate( trade = trade, market = markets[trade.marketId], - subaccount = subaccount, + subaccount = if (trade.marginMode == MarginMode.Isolated) subaccount else crossMarginSubaccount, user = user, input = input, ) @@ -109,7 +112,6 @@ internal class TradeInputCalculatorV2( rewardsParams: InternalRewardsParamsState?, feeTiers: List?, ): InternalTradeInputState { - val type = trade.type val marketId = market?.perpetualMarket?.id val position = if (marketId != null) { subaccount?.openPositions?.get(marketId) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/output/input/Input.kt b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/Input.kt index da67e534e..46347387f 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/output/input/Input.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/Input.kt @@ -6,6 +6,7 @@ import exchange.dydx.abacus.state.manager.V4Environment import exchange.dydx.abacus.utils.IList import exchange.dydx.abacus.utils.Logger import kollections.JsExport +import kollections.toIList import kotlinx.serialization.Serializable @JsExport @@ -48,7 +49,12 @@ data class Input( Logger.d { "creating Input\n" } data?.let { - val current = InputType.invoke(parser.asString(data["current"])) + val current = if (staticTyping) { + internalState?.input?.currentType + } else { + InputType.invoke(parser.asString(data["current"])) + } + val trade = if (staticTyping) { TradeInput.create(state = internalState?.input?.trade) } else { @@ -56,17 +62,27 @@ data class Input( } val closePosition = ClosePositionInput.create(existing?.closePosition, parser, parser.asMap(data["closePosition"])) + val transfer = TransferInput.create(existing?.transfer, parser, parser.asMap(data["transfer"]), environment, internalState?.transfer) + val triggerOrders = TriggerOrdersInput.create(existing?.triggerOrders, parser, parser.asMap(data["triggerOrders"])) + val adjustIsolatedMargin = AdjustIsolatedMarginInput.create(existing?.adjustIsolatedMargin, parser, parser.asMap(data["adjustIsolatedMargin"])) + val errors = ValidationError.create(existing?.errors, parser, parser.asList(data["errors"])) + val childSubaccountErrors = ValidationError.create(existing?.childSubaccountErrors, parser, parser.asList(data["childSubaccountErrors"])) - val receiptLines = ReceiptLine.create(parser, parser.asList(data["receiptLines"])) + + val receiptLines = if (staticTyping) { + internalState?.input?.receiptLines?.toIList() + } else { + ReceiptLine.create(parser, parser.asList(data["receiptLines"])) + } return if (existing?.current !== current || existing?.trade !== trade || diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/output/input/ReceiptLine.kt b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/ReceiptLine.kt index d5a6995b2..6a78aadf6 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/output/input/ReceiptLine.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/ReceiptLine.kt @@ -31,7 +31,7 @@ enum class ReceiptLine(val rawValue: String) { companion object { operator fun invoke(rawValue: String) = - ReceiptLine.values().firstOrNull { it.rawValue == rawValue } + entries.firstOrNull { it.rawValue == rawValue } internal fun create( parser: ParserProtocol, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt index 4827c6e7f..e848f3b68 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt @@ -24,6 +24,7 @@ import exchange.dydx.abacus.output.input.InputType import exchange.dydx.abacus.output.input.MarginMode import exchange.dydx.abacus.output.input.OrderSide import exchange.dydx.abacus.output.input.OrderType +import exchange.dydx.abacus.output.input.ReceiptLine import exchange.dydx.abacus.output.input.SelectionOption import exchange.dydx.abacus.output.input.Tooltip import exchange.dydx.abacus.output.input.TradeInputBracket @@ -54,6 +55,7 @@ internal data class InternalState( internal data class InternalInputState( var trade: InternalTradeInputState = InternalTradeInputState(), var currentType: InputType? = null, + var receiptLines: List? = null, ) internal data class InternalTradeInputState( diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt index fb8175656..f9bf4c62a 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt @@ -4,6 +4,7 @@ import exchange.dydx.abacus.calculator.AccountCalculator import exchange.dydx.abacus.calculator.AdjustIsolatedMarginInputCalculator import exchange.dydx.abacus.calculator.CalculationPeriod import exchange.dydx.abacus.calculator.MarketCalculator +import exchange.dydx.abacus.calculator.ReceiptCalculator import exchange.dydx.abacus.calculator.TradeCalculation import exchange.dydx.abacus.calculator.TradeInputCalculator import exchange.dydx.abacus.calculator.TransferInputCalculator @@ -131,6 +132,8 @@ open class TradingStateMachine( internal val accountCalculator = AccountCalculator(parser, useParentSubaccount) internal val accountCalculatorV2 = AccountCalculatorV2(parser, useParentSubaccount) + private val receiptCalculator = ReceiptCalculator() + internal val inputValidator = InputValidator(localizer, formatter, parser) internal var data: Map? = null @@ -931,17 +934,16 @@ open class TradingStateMachine( private fun recalculateStates(changes: StateChanges) { val subaccountNumbers = changes.subaccountNumbers ?: allSubaccountNumbers() if (changes.changes.contains(Changes.subaccount)) { - val periods = if (this.input != null) { - setOf( - CalculationPeriod.current, - CalculationPeriod.post, - CalculationPeriod.settled, - ) - } else { - setOf(CalculationPeriod.current) - } - if (staticTyping) { + val periods = if (internalState.input.currentType != null) { + setOf( + CalculationPeriod.current, + CalculationPeriod.post, + CalculationPeriod.settled, + ) + } else { + setOf(CalculationPeriod.current) + } internalState.wallet.account = accountCalculatorV2.calculate( account = internalState.wallet.account, subaccountNumbers = subaccountNumbers, @@ -952,6 +954,16 @@ open class TradingStateMachine( ) } this.marketsSummary?.let { marketsSummary -> + val periods = if (this.input != null) { + setOf( + CalculationPeriod.current, + CalculationPeriod.post, + CalculationPeriod.settled, + ) + } else { + setOf(CalculationPeriod.current) + } + parser.asNativeMap(marketsSummary["markets"])?.let { markets -> val modifiedAccount = accountCalculator.calculate( account = account, @@ -993,41 +1005,65 @@ open class TradingStateMachine( } if (changes.changes.contains(Changes.input)) { - val modified = this.input?.mutable() ?: return - when (parser.asString(modified["current"])) { - "trade" -> { - when (parser.asString(parser.value(modified, "trade.size.input"))) { - "size.size", "size.usdcSize" -> { - val subaccountNumber = changes.subaccountNumbers?.firstOrNull() - val marketId = parser.asString(parser.value(modified, "trade.marketId")) - if (subaccountNumber != null && marketId != null) { - val leverage = - parser.asDouble( - parser.value( - this.account, - "subaccounts.$subaccountNumber.openPositions.$marketId.leverage.postOrder", - ), - ) - modified.safeSet("trade.size.leverage", leverage) - } else { - modified.safeSet("trade.size.leverage", null) - } + if (staticTyping) { + // finalize the trade input leverage + if (internalState.input.currentType == InputType.TRADE) { + val trade = internalState.input.trade + val account = internalState.wallet.account + if (trade.size?.input == "size.size" || trade.size?.input == "size.usdcSize") { + val subaccountNumber = changes.subaccountNumbers?.firstOrNull() + val marketId = trade.marketId + if (subaccountNumber != null && marketId != null) { + val position = account.subaccounts[subaccountNumber]?.openPositions?.get(marketId) + val postOrderLeverage = position?.calculated?.get(CalculationPeriod.post)?.leverage + trade.size = trade.size?.copy(leverage = postOrderLeverage) + } else { + trade.size = trade.size?.copy(leverage = null) } + } + } + // calculate the receipt lines + receiptCalculator.calculate( + input = internalState.input, + ) + } else { + val modified = this.input?.mutable() ?: return + when (parser.asString(modified["current"])) { + "trade" -> { + when (parser.asString(parser.value(modified, "trade.size.input"))) { + "size.size", "size.usdcSize" -> { + val subaccountNumber = changes.subaccountNumbers?.firstOrNull() + val marketId = + parser.asString(parser.value(modified, "trade.marketId")) + if (subaccountNumber != null && marketId != null) { + val leverage = + parser.asDouble( + parser.value( + this.account, + "subaccounts.$subaccountNumber.openPositions.$marketId.leverage.postOrder", + ), + ) + modified.safeSet("trade.size.leverage", leverage) + } else { + modified.safeSet("trade.size.leverage", null) + } + } - else -> { + else -> { + } } } - } - "triggerOrders" -> { - // TODO: update price diffs based on price.input - } + "triggerOrders" -> { + // TODO: update price diffs based on price.input + } - "closePosition", "transfer" -> { + "closePosition", "transfer" -> { + } } + modified.safeSet("receiptLines", calculateReceipt(modified)) + this.input = modified } - modified.safeSet("receiptLines", calculateReceipt(modified)) - this.input = modified } } From 7034e0a5ac88201636a9cc73255f9090158c8608 Mon Sep 17 00:00:00 2001 From: Rui Date: Thu, 15 Aug 2024 14:57:12 -0700 Subject: [PATCH 42/63] Fix position list --- .../exchange.dydx.abacus/calculator/V2/AccountCalculatorV2.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/AccountCalculatorV2.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/AccountCalculatorV2.kt index 8bdd3e0a8..57d1dce99 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/AccountCalculatorV2.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/AccountCalculatorV2.kt @@ -106,7 +106,7 @@ internal class AccountCalculatorV2( childSubaccountNumber: Int, childSubaccount: InternalSubaccountState, ): InternalSubaccountState { - val parentOpenPositions = parentSubaccount.openPositions + val parentOpenPositions = parentSubaccount.childSubaccountOpenPositions ?: parentSubaccount.openPositions val modifiedOpenPositions = parentOpenPositions?.toMutableMap() ?: mutableMapOf() val childOpenPositions = childSubaccount.openPositions for ((market, childOpenPosition) in childOpenPositions ?: emptyMap()) { From 033b44e9ffe528d69ad13ea7c9a67d66c34aa3a4 Mon Sep 17 00:00:00 2001 From: Rui Date: Thu, 15 Aug 2024 17:17:46 -0700 Subject: [PATCH 43/63] Fix a position bug --- .../calculator/V2/AccountCalculatorV2.kt | 9 +++++---- .../calculator/V2/SubaccountCalculatorV2.kt | 3 ++- .../calculator/V2/SubaccountTransformerV2.kt | 4 ++-- .../output/account/Subaccount.kt | 2 +- .../wallet/account/SubaccountProcessor.kt | 16 +++++++++++----- .../state/internalstate/InternalState.kt | 10 ++-------- 6 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/AccountCalculatorV2.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/AccountCalculatorV2.kt index 57d1dce99..56aaff1ea 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/AccountCalculatorV2.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/AccountCalculatorV2.kt @@ -60,16 +60,17 @@ internal class AccountCalculatorV2( val subaccounts = account.subaccounts val subaccountNumbers = subaccounts.keys.sorted() + // merge child subaccounts with parent subaccount and store it into groupedSubaccounts val groupedSubaccounts = mutableMapOf() for (subaccountNumber in subaccountNumbers) { val subaccount = subaccounts[subaccountNumber] ?: continue if (subaccountNumber < NUM_PARENT_SUBACCOUNTS) { // this is a parent subaccount - groupedSubaccounts[subaccountNumber] = subaccount + groupedSubaccounts[subaccountNumber] = subaccount.copy() } else { val parentSubaccountNumber = subaccountNumber % NUM_PARENT_SUBACCOUNTS var parentSubaccount = groupedSubaccounts[parentSubaccountNumber] - ?: subaccounts[parentSubaccountNumber] + ?: subaccounts[parentSubaccountNumber]?.copy() ?: InternalSubaccountState(subaccountNumber = parentSubaccountNumber) parentSubaccount = mergeChildOpenPositions( @@ -106,7 +107,7 @@ internal class AccountCalculatorV2( childSubaccountNumber: Int, childSubaccount: InternalSubaccountState, ): InternalSubaccountState { - val parentOpenPositions = parentSubaccount.childSubaccountOpenPositions ?: parentSubaccount.openPositions + val parentOpenPositions = parentSubaccount.openPositions val modifiedOpenPositions = parentOpenPositions?.toMutableMap() ?: mutableMapOf() val childOpenPositions = childSubaccount.openPositions for ((market, childOpenPosition) in childOpenPositions ?: emptyMap()) { @@ -116,7 +117,7 @@ internal class AccountCalculatorV2( // ) modifiedOpenPositions[market] = childOpenPosition } - parentSubaccount.childSubaccountOpenPositions = modifiedOpenPositions + parentSubaccount.openPositions = modifiedOpenPositions return parentSubaccount } 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 09f868615..ea4bc97d6 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/SubaccountCalculatorV2.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/SubaccountCalculatorV2.kt @@ -92,6 +92,7 @@ internal class SubaccountCalculatorV2( initialRiskTotal += positionCalculated?.initialRiskTotal ?: Numeric.double.ZERO } + calculated.notionalTotal = notionalTotal calculated.valueTotal = valueTotal calculated.initialRiskTotal = initialRiskTotal @@ -312,7 +313,7 @@ internal class SubaccountCalculatorV2( price: Map?, periods: Set, ): InternalSubaccountState { - for ((key, position) in subaccount.positions ?: emptyMap()) { + for ((key, position) in subaccount.openPositions ?: emptyMap()) { val market = markets?.get(key) if (market != null) { calculatePositionValues( diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/SubaccountTransformerV2.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/SubaccountTransformerV2.kt index 4a62f6b47..53e051cb7 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/SubaccountTransformerV2.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/SubaccountTransformerV2.kt @@ -140,7 +140,7 @@ internal class SubaccountTransformerV2( hasTransfer: Boolean = false, ) { val deltaMarketId = delta?.marketId - val positions = subaccount.positions + val positions = subaccount.openPositions val marketPosition = positions?.get(deltaMarketId) val modifiedDelta = if (delta != null) { @@ -155,7 +155,7 @@ internal class SubaccountTransformerV2( } if (positions != null) { - subaccount.positions = applyDeltaToPositions( + subaccount.openPositions = applyDeltaToPositions( positions = positions, delta = modifiedDelta, period = period, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/output/account/Subaccount.kt b/src/commonMain/kotlin/exchange.dydx.abacus/output/account/Subaccount.kt index b3a1ab91a..76da5ff37 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/output/account/Subaccount.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/output/account/Subaccount.kt @@ -230,7 +230,7 @@ data class Subaccount( createOpenPositions( existing = existing?.openPositions, parser = parser, - openPositions = internalState?.groupedOpenPositions, + openPositions = internalState?.openPositions, subaccount = internalState, ) } else { 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 74e08d327..465a3bc5e 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 @@ -15,6 +15,7 @@ import exchange.dydx.abacus.utils.safeSet import indexer.codegen.IndexerAssetPositionResponseObject import indexer.codegen.IndexerFillResponseObject import indexer.codegen.IndexerPerpetualPositionResponseObject +import indexer.codegen.IndexerPerpetualPositionStatus import indexer.codegen.IndexerPnlTicksResponseObject import indexer.codegen.IndexerSubaccountResponseObject import indexer.codegen.IndexerTransferResponseObject @@ -240,17 +241,19 @@ internal open class SubaccountProcessor( existing.positions = perpetualPositionsProcessor.process( payload = payload.openPerpetualPositions, ) + existing.openPositions = existing.positions?.filterValues { + it.status == IndexerPerpetualPositionStatus.OPEN + } existing.assetPositions = assetPositionsProcessor.process( payload = payload.assetPositions, ) - - val subaccountCalculated = existing.calculated[CalculationPeriod.current] ?: InternalSubaccountCalculated() - existing.calculated[CalculationPeriod.current] = subaccountCalculated - subaccountCalculated.quoteBalance = subaccountCalculator.calculateQuoteBalance(existing.assetPositions) - existing.orders = null } + val subaccountCalculated = existing.calculated[CalculationPeriod.current] ?: InternalSubaccountCalculated() + existing.calculated[CalculationPeriod.current] = subaccountCalculated + subaccountCalculated.quoteBalance = subaccountCalculator.calculateQuoteBalance(existing.assetPositions) + return existing } @@ -489,6 +492,9 @@ internal open class SubaccountProcessor( existing = existing.positions, payload = payload, ) + existing.openPositions = existing.positions?.filterValues { + it.status == IndexerPerpetualPositionStatus.OPEN + } return existing } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt index e848f3b68..8bd838d56 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt @@ -227,20 +227,14 @@ internal data class InternalSubaccountState( var pendingPositions: List? = null, - // for parent subaccount only. This contains the consolidated open positions of all child subaccounts - var childSubaccountOpenPositions: Map? = null, + // calculated: For parent subaccount, this contains the calculated values of all child subaccounts + var openPositions: Map? = null, // Calculated: val calculated: MutableMap = mutableMapOf(), ) { val isParentSubaccount: Boolean get() = subaccountNumber < NUM_PARENT_SUBACCOUNTS - - val openPositions: Map? - get() = positions?.filterValues { it.status == IndexerPerpetualPositionStatus.OPEN } - - val groupedOpenPositions: Map? - get() = if (isParentSubaccount) childSubaccountOpenPositions else openPositions } internal data class InternalSubaccountCalculated( From e25d771d17f818b685800b15d25170755d9ff77a Mon Sep 17 00:00:00 2001 From: Rui Date: Thu, 15 Aug 2024 18:21:35 -0700 Subject: [PATCH 44/63] Fix issue with null position --- .../calculator/V2/SubaccountTransformerV2.kt | 40 ++++++++++++++----- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/SubaccountTransformerV2.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/SubaccountTransformerV2.kt index 53e051cb7..dd47bec4c 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/SubaccountTransformerV2.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/SubaccountTransformerV2.kt @@ -2,6 +2,8 @@ package exchange.dydx.abacus.calculator.v2 import abs import exchange.dydx.abacus.calculator.CalculationPeriod +import exchange.dydx.abacus.output.TradeStatesWithStringValues +import exchange.dydx.abacus.output.account.SubaccountPositionResources import exchange.dydx.abacus.output.input.OrderSide import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.internalstate.InternalMarketState @@ -317,23 +319,39 @@ internal class SubaccountTransformerV2( closedAt = null, exitPrice = null, subaccountNumber = null, - resources = null, + resources = SubaccountPositionResources( + sideStringKey = TradeStatesWithStringValues( + current = "APP.GENERAL.NONE", + postOrder = null, + postAllOrders = null, + ), + indicator = TradeStatesWithStringValues( + current = "none", + postOrder = null, + postAllOrders = null, + ), + sideString = TradeStatesWithStringValues( + current = null, + postOrder = null, + postAllOrders = null, + ), + ), calculated = mutableMapOf( CalculationPeriod.current to InternalPositionCalculated( valueTotal = 0.0, notionalTotal = 0.0, - adjustedImf = null, - adjustedMmf = null, - initialRiskTotal = null, - maxLeverage = null, - unrealizedPnl = null, - unrealizedPnlPercent = null, - marginValue = null, + adjustedImf = 0.0, + adjustedMmf = 0.0, + initialRiskTotal = 0.0, + maxLeverage = 0.0, + unrealizedPnl = 0.0, + unrealizedPnlPercent = 0.0, + marginValue = 0.0, realizedPnlPercent = 0.0, - leverage = null, + leverage = 0.0, size = 0.0, - liquidationPrice = null, - buyingPower = null, + liquidationPrice = 0.0, + buyingPower = 0.0, ), ), From 3b21a9037864e3d1c403e085b1aa39e94e4b26e4 Mon Sep 17 00:00:00 2001 From: Rui Date: Thu, 15 Aug 2024 19:36:53 -0700 Subject: [PATCH 45/63] Deep-copy to groupedSubaccount --- .../calculator/V2/AccountCalculatorV2.kt | 10 +++++---- .../TradeInputMarketOrderCalculator.kt | 8 +++---- .../state/internalstate/InternalState.kt | 21 +++++++++++++++++++ 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/AccountCalculatorV2.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/AccountCalculatorV2.kt index 56aaff1ea..e2528fd3e 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/AccountCalculatorV2.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/AccountCalculatorV2.kt @@ -60,17 +60,19 @@ internal class AccountCalculatorV2( val subaccounts = account.subaccounts val subaccountNumbers = subaccounts.keys.sorted() - // merge child subaccounts with parent subaccount and store it into groupedSubaccounts + // Merge child subaccounts with parent subaccount and store it into groupedSubaccounts + // We need to copy its entire content not just references, so that the subaccount calculations + // are not affected by the parent subaccount calculations. val groupedSubaccounts = mutableMapOf() for (subaccountNumber in subaccountNumbers) { val subaccount = subaccounts[subaccountNumber] ?: continue if (subaccountNumber < NUM_PARENT_SUBACCOUNTS) { - // this is a parent subaccount - groupedSubaccounts[subaccountNumber] = subaccount.copy() + // this is a parent subaccount.. + groupedSubaccounts[subaccountNumber] = subaccount.deepCopy() } else { val parentSubaccountNumber = subaccountNumber % NUM_PARENT_SUBACCOUNTS var parentSubaccount = groupedSubaccounts[parentSubaccountNumber] - ?: subaccounts[parentSubaccountNumber]?.copy() + ?: subaccounts[parentSubaccountNumber]?.deepCopy() ?: InternalSubaccountState(subaccountNumber = parentSubaccountNumber) parentSubaccount = mergeChildOpenPositions( 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 59d863682..0ea09193b 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 @@ -153,10 +153,10 @@ internal class TradeInputMarketOrderCalculator( "size.leverage" -> { val leverage = tradeSize.leverage ?: return null createMarketOrderFromLeverage( - leverage, - market, - subaccount, - user, + leverage = leverage, + market = market, + subaccount = subaccount, + user = user, ) } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt index 8bd838d56..88c69ed02 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt @@ -235,6 +235,27 @@ internal data class InternalSubaccountState( ) { val isParentSubaccount: Boolean get() = subaccountNumber < NUM_PARENT_SUBACCOUNTS + + fun deepCopy(): InternalSubaccountState { + return InternalSubaccountState( + fills = fills?.map { it.copy() }, + orders = orders?.map { it.copy() }, + transfers = transfers?.map { it.copy() }, + historicalPNLs = historicalPNLs?.map { it.copy() }, + positions = positions?.map { it.key to it.value.copy() }?.toMap(), + assetPositions = assetPositions?.map { it.key to it.value.copy() }?.toMap(), + subaccountNumber = subaccountNumber, + address = address, + equity = equity, + freeCollateral = freeCollateral, + marginEnabled = marginEnabled, + updatedAtHeight = updatedAtHeight, + latestProcessedBlockHeight = latestProcessedBlockHeight, + pendingPositions = pendingPositions?.map { it.copy() }, + openPositions = openPositions?.map { it.key to it.value.copy() }?.toMap(), + calculated = calculated.map { it.key to it.value.copy() }.toMap().toMutableMap(), + ) + } } internal data class InternalSubaccountCalculated( From 7b8791e15361f00fc902a12d1851a84830112e3b Mon Sep 17 00:00:00 2001 From: Rui Date: Mon, 19 Aug 2024 06:36:03 +0900 Subject: [PATCH 46/63] WIP --- .../TradeInput/TradeInputOptionsCalculator.kt | 6 +- .../output/input/Input.kt | 42 ++++-- .../output/input/ValidationError.kt | 4 +- .../state/internalstate/InternalState.kt | 16 ++- .../model/TradingStateMachine+TradeInput.kt | 7 +- .../state/model/TradingStateMachine.kt | 64 +++++---- .../validator/AccountInputValidator.kt | 66 +++++---- .../validator/BaseInputValidator.kt | 103 +++++++++++++- .../validator/FieldsInputValidator.kt | 17 ++- .../validator/InputValidator.kt | 128 +++++++++++++++--- .../validator/TradeInputValidator.kt | 119 +++++----------- .../validator/TransferInputValidator.kt | 62 +++++---- .../validator/TriggerOrdersInputValidator.kt | 109 ++++++++------- .../validator/ValidatorProtocols.kt | 24 +++- .../trade/TradeAccountStateValidator.kt | 55 ++++---- .../trade/TradeBracketOrdersValidator.kt | 43 +++--- .../trade/TradeInputDataValidator.kt | 71 +++++----- .../trade/TradeMarketOrderInputValidator.kt | 24 ++-- .../trade/TradePositionStateValidator.kt | 59 ++++---- .../trade/TradeResctrictedValidator.kt | 111 +++++++++++++++ .../trade/TradeTriggerPriceValidator.kt | 78 ++++++----- .../validator/transfer/DepositValidator.kt | 10 +- .../transfer/TransferOutValidator.kt | 24 ++-- .../transfer/WithdrawalCapacityValidator.kt | 23 +++- .../transfer/WithdrawalGatingValidator.kt | 23 +++- 25 files changed, 861 insertions(+), 427 deletions(-) create mode 100644 src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeResctrictedValidator.kt diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputOptionsCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputOptionsCalculator.kt index d9578087b..6f52a0200 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputOptionsCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputOptionsCalculator.kt @@ -407,7 +407,11 @@ internal class TradeInputOptionsCalculator( } private fun trailingPercentField(): Map { - return mapOf("field" to "price.trailingPercent", "type" to "double") + return mapOf( + "" + + "field" to "price.trailingPercent", + "type" to "double", + ) } private fun reduceOnlyField(): Map? { diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/output/input/Input.kt b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/Input.kt index 46347387f..db3c2fabb 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/output/input/Input.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/Input.kt @@ -48,40 +48,58 @@ data class Input( ): Input? { Logger.d { "creating Input\n" } - data?.let { + if (staticTyping || data != null) { val current = if (staticTyping) { internalState?.input?.currentType } else { - InputType.invoke(parser.asString(data["current"])) + InputType.invoke(parser.asString(data?.get("current"))) } val trade = if (staticTyping) { TradeInput.create(state = internalState?.input?.trade) } else { - TradeInput.create(existing?.trade, parser, parser.asMap(data["trade"])) + TradeInput.create(existing?.trade, parser, parser.asMap(data?.get("trade"))) } val closePosition = - ClosePositionInput.create(existing?.closePosition, parser, parser.asMap(data["closePosition"])) + ClosePositionInput.create(existing?.closePosition, parser, parser.asMap(data?.get("closePosition"))) val transfer = - TransferInput.create(existing?.transfer, parser, parser.asMap(data["transfer"]), environment, internalState?.transfer) + TransferInput.create(existing?.transfer, parser, parser.asMap(data?.get("transfer")), environment, internalState?.transfer) val triggerOrders = - TriggerOrdersInput.create(existing?.triggerOrders, parser, parser.asMap(data["triggerOrders"])) + TriggerOrdersInput.create(existing?.triggerOrders, parser, parser.asMap(data?.get("triggerOrders"))) val adjustIsolatedMargin = - AdjustIsolatedMarginInput.create(existing?.adjustIsolatedMargin, parser, parser.asMap(data["adjustIsolatedMargin"])) + AdjustIsolatedMarginInput.create( + existing?.adjustIsolatedMargin, + parser, + parser.asMap( + data?.get("adjustIsolatedMargin"), + ), + ) - val errors = - ValidationError.create(existing?.errors, parser, parser.asList(data["errors"])) + val errors = if (staticTyping) { + internalState?.input?.errors?.toIList() + } else { + ValidationError.create(existing?.errors, parser, parser.asList(data?.get("errors"))) + } - val childSubaccountErrors = - ValidationError.create(existing?.childSubaccountErrors, parser, parser.asList(data["childSubaccountErrors"])) + val childSubaccountErrors = if (staticTyping) { + internalState?.input?.childSubaccountErrors?.toIList() + } else { + ValidationError.create( + existing?.childSubaccountErrors, + parser, + parser.asList( + data?.get("childSubaccountErrors"), + ), + ) + } val receiptLines = if (staticTyping) { internalState?.input?.receiptLines?.toIList() } else { - ReceiptLine.create(parser, parser.asList(data["receiptLines"])) + ReceiptLine.create(parser, parser.asList(data?.get("receiptLines"))) } return if (existing?.current !== current || diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/output/input/ValidationError.kt b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/ValidationError.kt index 425c34e36..02949506b 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/output/input/ValidationError.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/ValidationError.kt @@ -142,7 +142,7 @@ enum class ErrorType(val rawValue: String) { companion object { operator fun invoke(rawValue: String) = - ErrorType.values().firstOrNull { it.rawValue == rawValue } + entries.firstOrNull { it.rawValue == rawValue } } } @@ -154,7 +154,7 @@ enum class ErrorAction(val rawValue: String) { companion object { operator fun invoke(rawValue: String) = - ErrorAction.values().firstOrNull { it.rawValue == rawValue } + entries.firstOrNull { it.rawValue == rawValue } } } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt index 88c69ed02..a2e1b4cc8 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt @@ -33,6 +33,7 @@ import exchange.dydx.abacus.output.input.TradeInputGoodUntil import exchange.dydx.abacus.output.input.TradeInputMarketOrder import exchange.dydx.abacus.output.input.TradeInputPrice import exchange.dydx.abacus.output.input.TradeInputSize +import exchange.dydx.abacus.output.input.ValidationError import exchange.dydx.abacus.state.manager.HistoricalTradingRewardsPeriod import exchange.dydx.abacus.utils.NUM_PARENT_SUBACCOUNTS import indexer.codegen.IndexerHistoricalBlockTradingReward @@ -54,9 +55,20 @@ internal data class InternalState( internal data class InternalInputState( var trade: InternalTradeInputState = InternalTradeInputState(), - var currentType: InputType? = null, var receiptLines: List? = null, -) + var errors: List? = null, + var childSubaccountErrors: List? = null, +) { + var currentType: InputType? = null + set(value) { + if (field != value) { + receiptLines = null + errors = null + childSubaccountErrors = null + field = value + } + } +} internal data class InternalTradeInputState( var marketId: String? = null, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt index c60eb1dd6..0d9e55261 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt @@ -11,6 +11,7 @@ import exchange.dydx.abacus.responses.StateResponse import exchange.dydx.abacus.responses.cannotModify import exchange.dydx.abacus.state.changes.Changes import exchange.dydx.abacus.state.changes.StateChanges +import exchange.dydx.abacus.state.internalstate.InternalTradeInputState import exchange.dydx.abacus.utils.mutable import exchange.dydx.abacus.utils.mutableMapOf import exchange.dydx.abacus.utils.safeSet @@ -18,6 +19,10 @@ import kollections.JsExport import kollections.iListOf import kotlinx.serialization.Serializable +internal interface InputFieldProtocol { + val test: ((InternalTradeInputState) -> Any?)? +} + @JsExport @Serializable enum class TradeInputField(val rawValue: String) { @@ -55,7 +60,7 @@ enum class TradeInputField(val rawValue: String) { companion object { operator fun invoke(rawValue: String?) = - TradeInputField.values().firstOrNull { it.rawValue == rawValue } + entries.firstOrNull { it.rawValue == rawValue } } internal val tradeDataOption: String? diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt index f9bf4c62a..696069c96 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt @@ -636,19 +636,21 @@ open class TradingStateMachine( null } - this.input = inputValidator.validate( - staticTyping = staticTyping, - internalState = this.internalState, - subaccountNumber = subaccountNumber, - wallet = this.wallet, - user = this.user, - subaccount = subaccount, - markets = parser.asNativeMap(this.marketsSummary?.get("markets")), - input = this.input, - configs = this.configs, - currentBlockAndHeight = this.currentBlockAndHeight, - environment = this.environment, - ) + if (!staticTyping) { + // Skip this for static typing.. since the validator will be called in updateState(). + // No need to call this twice. + this.input = inputValidator.validateDeprecated( + subaccountNumber = subaccountNumber, + wallet = this.wallet, + user = this.user, + subaccount = subaccount, + markets = parser.asNativeMap(this.marketsSummary?.get("markets")), + input = this.input, + configs = this.configs, + currentBlockAndHeight = this.currentBlockAndHeight, + environment = this.environment, + ) + } if (subaccountNumber != null) { if (staticTyping) { @@ -656,7 +658,7 @@ open class TradingStateMachine( InputType.TRADE -> { calculateTrade(subaccountNumber) } - InputType.TRADE -> { + InputType.TRANSFER -> { calculateTransfer(subaccountNumber) } InputType.TRIGGER_ORDERS -> { @@ -1524,19 +1526,27 @@ open class TradingStateMachine( } if (changes.changes.contains(Changes.input)) { - this.input = inputValidator.validate( - staticTyping = staticTyping, - internalState = internalState, - subaccountNumber = subaccountNumber, - wallet = this.wallet, - user = this.user, - subaccount = subaccount, - markets = parser.asNativeMap(this.marketsSummary?.get("markets")), - input = this.input, - configs = this.configs, - currentBlockAndHeight = this.currentBlockAndHeight, - environment = this.environment, - ) + if (staticTyping) { + inputValidator.validate( + internalState = internalState, + subaccountNumber = subaccountNumber, + currentBlockAndHeight = currentBlockAndHeight, + environment = environment, + ) + } else { + this.input = inputValidator.validateDeprecated( + subaccountNumber = subaccountNumber, + wallet = this.wallet, + user = this.user, + subaccount = subaccount, + markets = parser.asNativeMap(this.marketsSummary?.get("markets")), + input = this.input, + configs = this.configs, + currentBlockAndHeight = this.currentBlockAndHeight, + environment = this.environment, + ) + } + this.input?.let { input = Input.create( existing = input, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/AccountInputValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/AccountInputValidator.kt index 2a670082b..cb0360c68 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/AccountInputValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/AccountInputValidator.kt @@ -1,5 +1,7 @@ package exchange.dydx.abacus.validator +import exchange.dydx.abacus.output.input.InputType +import exchange.dydx.abacus.output.input.ValidationError import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.app.helper.Formatter @@ -14,8 +16,16 @@ internal class AccountInputValidator( parser: ParserProtocol, ) : BaseInputValidator(localizer, formatter, parser), ValidatorProtocol { override fun validate( - staticTyping: Boolean, internalState: InternalState, + subaccountNumber: Int?, + currentBlockAndHeight: BlockAndTime?, + inputType: InputType, + environment: V4Environment?, + ): List? { + TODO("Not yet implemented") + } + + override fun validateDeprecated( wallet: Map?, user: Map?, subaccount: Map?, @@ -40,15 +50,15 @@ internal class AccountInputValidator( return if (wallet != null) { null } else { - error( - "ERROR", - "REQUIRED_WALLET", - null, - "ERRORS.TRADE_BOX_TITLE.CONNECT_WALLET_TO_TRADE", - "ERRORS.TRADE_BOX_TITLE.CONNECT_WALLET_TO_TRADE", - "ERRORS.TRADE_BOX.CONNECT_WALLET_TO_TRADE", - null, - "/onboard", + errorDeprecated( + type = "ERROR", + errorCode = "REQUIRED_WALLET", + fields = null, + actionStringKey = "ERRORS.TRADE_BOX_TITLE.CONNECT_WALLET_TO_TRADE", + titleStringKey = "ERRORS.TRADE_BOX_TITLE.CONNECT_WALLET_TO_TRADE", + textStringKey = "ERRORS.TRADE_BOX.CONNECT_WALLET_TO_TRADE", + textParams = null, + action = "/onboard", ) } } @@ -61,15 +71,15 @@ internal class AccountInputValidator( return if (account != null) { null } else { - error( - "ERROR", - "REQUIRED_ACCOUNT", - null, - "ERRORS.TRADE_BOX_TITLE.DEPOSIT_TO_TRADE", - "ERRORS.TRADE_BOX_TITLE.DEPOSIT_TO_TRADE", - "ERRORS.TRADE_BOX.DEPOSIT_TO_TRADE", - null, - "/deposit", + errorDeprecated( + type = "ERROR", + errorCode = "REQUIRED_ACCOUNT", + fields = null, + actionStringKey = "ERRORS.TRADE_BOX_TITLE.DEPOSIT_TO_TRADE", + titleStringKey = "ERRORS.TRADE_BOX_TITLE.DEPOSIT_TO_TRADE", + textStringKey = "ERRORS.TRADE_BOX.DEPOSIT_TO_TRADE", + textParams = null, + action = "/deposit", ) } } @@ -89,15 +99,15 @@ internal class AccountInputValidator( // subaccountNumber is null when a childSubaccount has not been created yet null } else { - error( - "ERROR", - "NO_EQUITY_DEPOSIT_FIRST", - null, - "ERRORS.TRADE_BOX_TITLE.NO_EQUITY_DEPOSIT_FIRST", - "ERRORS.TRADE_BOX_TITLE.NO_EQUITY_DEPOSIT_FIRST", - "ERRORS.TRADE_BOX.NO_EQUITY_DEPOSIT_FIRST", - null, - "/deposit", + errorDeprecated( + type = "ERROR", + errorCode = "NO_EQUITY_DEPOSIT_FIRST", + fields = null, + actionStringKey = "ERRORS.TRADE_BOX_TITLE.NO_EQUITY_DEPOSIT_FIRST", + titleStringKey = "ERRORS.TRADE_BOX_TITLE.NO_EQUITY_DEPOSIT_FIRST", + textStringKey = "ERRORS.TRADE_BOX.NO_EQUITY_DEPOSIT_FIRST", + textParams = null, + action = "/deposit", ) } } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/BaseInputValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/BaseInputValidator.kt index 1c9f12f01..3befa132c 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/BaseInputValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/BaseInputValidator.kt @@ -1,11 +1,19 @@ package exchange.dydx.abacus.validator +import exchange.dydx.abacus.output.input.ErrorAction +import exchange.dydx.abacus.output.input.ErrorParam +import exchange.dydx.abacus.output.input.ErrorResources +import exchange.dydx.abacus.output.input.ErrorString +import exchange.dydx.abacus.output.input.ErrorType +import exchange.dydx.abacus.output.input.ValidationError import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.app.helper.Formatter import exchange.dydx.abacus.utils.JsonEncoder import exchange.dydx.abacus.utils.filterNotNull import exchange.dydx.abacus.utils.mutable +import kollections.iListOf +import kollections.toIList internal open class BaseInputValidator( internal val localizer: LocalizerProtocol?, @@ -13,7 +21,32 @@ internal open class BaseInputValidator( val parser: ParserProtocol, ) { private val jsonEncoder = JsonEncoder() - internal fun required( + + fun required( + errorCode: String, + field: String, + actionStringKey: String, + ): ValidationError { + return ValidationError( + code = errorCode, + type = ErrorType.required, + fields = iListOf(field), + action = null, + link = null, + linkText = null, + resources = ErrorResources( + title = null, + text = null, + action = ErrorString( + stringKey = actionStringKey, + params = null, + localized = null, + ), + ), + ) + } + + internal fun requiredDeprecated( errorCode: String, field: String, actionStringKey: String, @@ -30,7 +63,50 @@ internal open class BaseInputValidator( ) } - internal fun error( + fun error( + type: ErrorType, + errorCode: String, + fields: List?, + actionStringKey: String?, + titleStringKey: String, + textStringKey: String, + textParams: Map? = null, + action: ErrorAction? = null, + link: String? = null, + linkText: String? = null, + ): ValidationError { + return ValidationError( + code = errorCode, + type = type, + fields = fields?.toIList(), + action = action, + link = link, + linkText = linkText, + resources = ErrorResources( + title = ErrorString( + stringKey = titleStringKey, + params = null, + localized = localize(titleStringKey), + ), + text = ErrorString( + stringKey = textStringKey, + params = params(parser, textParams)?.toIList(), + localized = localize(titleStringKey, textParams), + ), + action = if (actionStringKey != null) { + ErrorString( + stringKey = actionStringKey, + params = null, + localized = null, + ) + } else { + null + }, + ), + ) + } + + internal fun errorDeprecated( type: String, errorCode: String, fields: List?, @@ -57,7 +133,7 @@ internal open class BaseInputValidator( "text" to listOfNotNull( localize(textStringKey, textParams)?.let { "localized" to it } ?: run { null }, "stringKey" to textStringKey, - "params" to params(parser, textParams), + "params" to paramsDeprecated(parser, textParams), ).toMap(), "action" to listOfNotNull( localize(actionStringKey, null)?.let { "localized" to it } ?: run { null }, @@ -117,6 +193,27 @@ internal open class BaseInputValidator( private fun params( parser: ParserProtocol, map: Map?, + ): List? { + if (map != null) { + val params = mutableListOf() + for ((key, value) in map) { + parser.asNativeMap(value)?.let { + val param = ErrorParam( + key = key, + value = parser.asString(it["value"]), + format = parser.asString(it["format"]), + ) + params.add(param) + } + } + return params + } + return null + } + + private fun paramsDeprecated( + parser: ParserProtocol, + map: Map?, ): List>? { if (map != null) { val params = mutableListOf>() diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/FieldsInputValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/FieldsInputValidator.kt index e002b188c..adaca975a 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/FieldsInputValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/FieldsInputValidator.kt @@ -1,5 +1,7 @@ package exchange.dydx.abacus.validator +import exchange.dydx.abacus.output.input.InputType +import exchange.dydx.abacus.output.input.ValidationError import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.app.helper.Formatter @@ -11,11 +13,18 @@ internal class FieldsInputValidator( localizer: LocalizerProtocol?, formatter: Formatter?, parser: ParserProtocol, -) : - BaseInputValidator(localizer, formatter, parser), ValidatorProtocol { +) : BaseInputValidator(localizer, formatter, parser), ValidatorProtocol { override fun validate( - staticTyping: Boolean, internalState: InternalState, + subaccountNumber: Int?, + currentBlockAndHeight: BlockAndTime?, + inputType: InputType, + environment: V4Environment?, + ): List? { + TODO("Not yet implemented") + } + + override fun validateDeprecated( wallet: Map?, user: Map?, subaccount: Map?, @@ -63,7 +72,7 @@ internal class FieldsInputValidator( val errorCode = errorCode(field) val errorStringKey = errorStringKey(transaction, transactionType, field) if (errorCode != null && errorStringKey != null) { - required(errorCode, field, errorStringKey) + requiredDeprecated(errorCode, field, errorStringKey) } else { null } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/InputValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/InputValidator.kt index 36efe5dbd..e79a23c6a 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/InputValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/InputValidator.kt @@ -1,5 +1,7 @@ package exchange.dydx.abacus.validator +import exchange.dydx.abacus.output.input.InputType +import exchange.dydx.abacus.output.input.ValidationError import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.app.helper.Formatter @@ -15,8 +17,8 @@ internal class InputValidator( val parser: ParserProtocol, ) { private val errorTypeLookup = mapOf( - "ERROR" to 0, - "REQUIRED" to 1, + "REQUIRED" to 0, + "ERROR" to 1, "WARNING" to 2, ) private val errorCodeLookup = mapOf( @@ -87,8 +89,29 @@ internal class InputValidator( ) fun validate( - staticTyping: Boolean, internalState: InternalState, + subaccountNumber: Int?, + currentBlockAndHeight: BlockAndTime?, + environment: V4Environment?, + ) { + val errors = sort( + validateTransaction( + internalState = internalState, + subaccountNumber = subaccountNumber, + currentBlockAndHeight = currentBlockAndHeight, + environment = environment, + ), + ) + + val isChildSubaccount = subaccountNumber != null && subaccountNumber >= NUM_PARENT_SUBACCOUNTS + if (isChildSubaccount) { + internalState.input.childSubaccountErrors = errors + } else { + internalState.input.errors = errors + } + } + + fun validateDeprecated( subaccountNumber: Int?, wallet: Map?, user: Map?, @@ -104,10 +127,8 @@ internal class InputValidator( val transaction = parser.asNativeMap(input[transactionType]) ?: return input val isChildSubaccount = subaccountNumber != null && subaccountNumber >= NUM_PARENT_SUBACCOUNTS - val errors = sort( - validateTransaction( - staticTyping = staticTyping, - internalState = internalState, + val errors = sortDeprecated( + validateTransactionDeprecated( wallet = wallet, user = user, subaccount = subaccount, @@ -140,8 +161,32 @@ internal class InputValidator( } private fun validateTransaction( - staticTyping: Boolean, internalState: InternalState, + subaccountNumber: Int?, + currentBlockAndHeight: BlockAndTime?, + environment: V4Environment?, + ): List? { + val inputType = internalState.input.currentType ?: return null + val validators = validatorsFor(inputType) + if (validators.isNullOrEmpty()) { + return null + } + + val result: MutableList = mutableListOf() + for (validator in validators) { + val validatorErrors = validator.validate( + internalState = internalState, + subaccountNumber = subaccountNumber, + currentBlockAndHeight = currentBlockAndHeight, + inputType = inputType, + environment = environment, + ) ?: emptyList() + result.addAll(validatorErrors) + } + return result + } + + private fun validateTransactionDeprecated( wallet: Map?, user: Map?, subaccount: Map?, @@ -152,14 +197,12 @@ internal class InputValidator( transactionType: String, environment: V4Environment?, ): List? { - val validators = validatorsFor(transactionType) + val validators = validatorsForDeprecated(transactionType) return if (validators != null) { val result = mutableListOf() for (validator in validators) { val validatorErrors = - validator.validate( - staticTyping = staticTyping, - internalState = internalState, + validator.validateDeprecated( wallet = wallet, user = user, subaccount = subaccount, @@ -180,7 +223,16 @@ internal class InputValidator( } } - private fun validatorsFor(transactionType: String): List? { + private fun validatorsFor(inputType: InputType): List? { + return when (inputType) { + InputType.TRADE -> tradeValidators + InputType.TRANSFER -> transferValidators + InputType.CLOSE_POSITION -> closePositionValidators + InputType.ADJUST_ISOLATED_MARGIN -> null + InputType.TRIGGER_ORDERS -> triggerOrdersValidators + } + } + private fun validatorsForDeprecated(transactionType: String): List? { return when (transactionType) { "closePosition" -> closePositionValidators "transfer" -> transferValidators @@ -190,7 +242,51 @@ internal class InputValidator( } } - private fun sort(errors: List?): List? { + private fun sort(errors: List?): List? { + if (errors == null) { + return null + } + + return errors.sortedWith { error1, error2 -> + val type1 = error1.type + val type2 = error2.type + if (type1 == type2) { + val code1 = errorCodeLookup[error1.code] + val code2 = errorCodeLookup[error2.code] + if (code1 != null) { + if (code2 != null) { + code1 - code2 + } else { + 1 + } + } else { + if (code2 != null) { + -1 + } else { + 0 + } + } + } else { + val typeCode1 = errorTypeLookup[type1.rawValue] + val typeCode2 = errorTypeLookup[type2.rawValue] + if (typeCode1 != null) { + if (typeCode2 != null) { + typeCode1 - typeCode2 + } else { + 1 + } + } else { + if (typeCode2 != null) { + -1 + } else { + 0 + } + } + } + } + } + + private fun sortDeprecated(errors: List?): List? { return if (errors != null) { return errors.sortedWith { error1, error2 -> val typeString1 = parser.asString(parser.value(error1, "type")) @@ -214,8 +310,8 @@ internal class InputValidator( } } } else { - val type1 = if (typeString1 != null) errorCodeLookup[typeString1] else null - val type2 = if (typeString2 != null) errorCodeLookup[typeString2] else null + val type1 = if (typeString1 != null) errorTypeLookup[typeString1] else null + val type2 = if (typeString2 != null) errorTypeLookup[typeString2] else null if (type1 != null) { if (type2 != null) { type1 - type2 diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/TradeInputValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/TradeInputValidator.kt index c6938c62e..9be81192c 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/TradeInputValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/TradeInputValidator.kt @@ -1,5 +1,7 @@ package exchange.dydx.abacus.validator +import exchange.dydx.abacus.output.input.InputType +import exchange.dydx.abacus.output.input.ValidationError import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.app.helper.Formatter @@ -12,15 +14,16 @@ import exchange.dydx.abacus.validator.trade.TradeBracketOrdersValidator import exchange.dydx.abacus.validator.trade.TradeInputDataValidator import exchange.dydx.abacus.validator.trade.TradeMarketOrderInputValidator import exchange.dydx.abacus.validator.trade.TradePositionStateValidator +import exchange.dydx.abacus.validator.trade.TradeResctrictedValidator import exchange.dydx.abacus.validator.trade.TradeTriggerPriceValidator internal class TradeInputValidator( localizer: LocalizerProtocol?, formatter: Formatter?, parser: ParserProtocol -) : - BaseInputValidator(localizer, formatter, parser), ValidatorProtocol { +) : BaseInputValidator(localizer, formatter, parser), ValidatorProtocol { private val tradeValidators = listOf( + TradeResctrictedValidator(localizer, formatter, parser), TradeInputDataValidator(localizer, formatter, parser), TradeMarketOrderInputValidator(localizer, formatter, parser), TradeBracketOrdersValidator(localizer, formatter, parser), @@ -30,8 +33,35 @@ internal class TradeInputValidator( ) override fun validate( - staticTyping: Boolean, internalState: InternalState, + subaccountNumber: Int?, + currentBlockAndHeight: BlockAndTime?, + inputType: InputType, + environment: V4Environment?, + ): List? { + val transactionType = internalState.input.currentType + if (transactionType != InputType.TRANSFER || transactionType != InputType.CLOSE_POSITION) { + return null + } + + val errors = mutableListOf() + for (validator in tradeValidators) { + val validatorErrors = + validator.validateTrade( + internalState = internalState, + change = PositionChange.NONE, + restricted = false, + environment = environment, + ) + if (validatorErrors != null) { + errors.addAll(validatorErrors) + } + } + + return errors + } + + override fun validateDeprecated( wallet: Map?, user: Map?, subaccount: Map?, @@ -49,23 +79,9 @@ internal class TradeInputValidator( val market = parser.asNativeMap(markets?.get(marketId)) val errors = mutableListOf() - val closeOnlyError = - validateClosingOnly( - parser, - subaccount, - market, - transaction, - change, - restricted, - ) - if (closeOnlyError != null) { - errors.add(closeOnlyError) - } for (validator in tradeValidators) { val validatorErrors = - validator.validateTrade( - staticTyping = staticTyping, - internalState = internalState, + validator.validateTradeDeprecated( subaccount = subaccount, market = market, configs = configs, @@ -129,71 +145,4 @@ internal class TradeInputValidator( } } } - - private fun validateClosingOnly( - parser: ParserProtocol, - subaccount: Map?, - market: Map?, - trade: Map, - change: PositionChange, - restricted: Boolean, - ): Map? { - val marketId = parser.asNativeMap(market?.get("assetId")) ?: "" - val canTrade = parser.asBool(parser.value(market, "status.canTrade")) ?: true - val canReduce = parser.asBool(parser.value(market, "status.canTrade")) ?: true - return if (canTrade) { - if (restricted) { - when (change) { - PositionChange.NEW, PositionChange.INCREASING, PositionChange.CROSSING -> - error( - "ERROR", - "RESTRICTED_USER", - null, - null, - "ERRORS.TRADE_BOX_TITLE.MARKET_ORDER_CLOSE_POSITION_ONLY", - "ERRORS.TRADE_BOX.MARKET_ORDER_CLOSE_POSITION_ONLY", - ) - - else -> null - } - } else { - return null - } - } else if (canReduce) { - when (change) { - PositionChange.NEW, PositionChange.INCREASING, PositionChange.CROSSING -> - error( - "ERROR", - "CLOSE_ONLY_MARKET", - listOf("size.size"), - "APP.TRADE.MODIFY_SIZE_FIELD", - "WARNINGS.TRADE_BOX_TITLE.MARKET_STATUS_CLOSE_ONLY", - "WARNINGS.TRADE_BOX.MARKET_STATUS_CLOSE_ONLY", - mapOf( - "MARKET" to mapOf( - "value" to marketId, - "format" to "string", - ), - ), - ) - - else -> null - } - } else { - error( - "ERROR", - "CLOSED_MARKET", - null, - null, - "WARNINGS.TRADE_BOX_TITLE.MARKET_STATUS_CLOSE_ONLY", - "WARNINGS.TRADE_BOX.MARKET_STATUS_CLOSE_ONLY", - mapOf( - "MARKET" to mapOf( - "value" to marketId, - "format" to "string", - ), - ), - ) - } - } } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/TransferInputValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/TransferInputValidator.kt index 3791f0993..00ef3310d 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/TransferInputValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/TransferInputValidator.kt @@ -1,5 +1,7 @@ package exchange.dydx.abacus.validator +import exchange.dydx.abacus.output.input.InputType +import exchange.dydx.abacus.output.input.ValidationError import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.app.helper.Formatter @@ -24,8 +26,16 @@ internal class TransferInputValidator( ) override fun validate( - staticTyping: Boolean, internalState: InternalState, + subaccountNumber: Int?, + currentBlockAndHeight: BlockAndTime?, + inputType: InputType, + environment: V4Environment?, + ): List? { + TODO("Not yet implemented") + } + + override fun validateDeprecated( wallet: Map?, user: Map?, subaccount: Map?, @@ -41,9 +51,7 @@ internal class TransferInputValidator( val restricted = parser.asBool(user?.get("restricted")) ?: false for (validator in transferValidators) { val validatorErrors = - validator.validateTransfer( - staticTyping = staticTyping, - internalState = internalState, + validator.validateTransferDeprecated( wallet = wallet, subaccount = subaccount, transfer = transaction, @@ -75,13 +83,13 @@ internal class TransferInputValidator( if (restricted) { when (change) { PositionChange.NEW, PositionChange.INCREASING, PositionChange.CROSSING -> - error( - "ERROR", - "RESTRICTED_USER", - listOf("size.size"), - "APP.TRADE.MODIFY_SIZE_FIELD", - "ERRORS.TRADE_BOX_TITLE.MARKET_ORDER_CLOSE_POSITION_ONLY", - "ERRORS.TRADE_BOX.MARKET_ORDER_CLOSE_POSITION_ONLY", + errorDeprecated( + type = "ERROR", + errorCode = "RESTRICTED_USER", + fields = listOf("size.size"), + actionStringKey = "APP.TRADE.MODIFY_SIZE_FIELD", + titleStringKey = "ERRORS.TRADE_BOX_TITLE.MARKET_ORDER_CLOSE_POSITION_ONLY", + textStringKey = "ERRORS.TRADE_BOX.MARKET_ORDER_CLOSE_POSITION_ONLY", ) else -> null @@ -92,14 +100,14 @@ internal class TransferInputValidator( } else if (canReduce) { when (change) { PositionChange.NEW, PositionChange.INCREASING, PositionChange.CROSSING -> - error( - "ERROR", - "CLOSE_ONLY_MARKET", - listOf("size.size"), - "APP.TRADE.MODIFY_SIZE_FIELD", - "WARNINGS.TRADE_BOX_TITLE.MARKET_STATUS_CLOSE_ONLY", - "WARNINGS.TRADE_BOX.MARKET_STATUS_CLOSE_ONLY", - mapOf( + errorDeprecated( + type = "ERROR", + errorCode = "CLOSE_ONLY_MARKET", + fields = listOf("size.size"), + actionStringKey = "APP.TRADE.MODIFY_SIZE_FIELD", + titleStringKey = "WARNINGS.TRADE_BOX_TITLE.MARKET_STATUS_CLOSE_ONLY", + textStringKey = "WARNINGS.TRADE_BOX.MARKET_STATUS_CLOSE_ONLY", + textParams = mapOf( "MARKET" to mapOf( "value" to marketId, "format" to "string", @@ -110,14 +118,14 @@ internal class TransferInputValidator( else -> null } } else { - error( - "ERROR", - "CLOSED_MARKET", - null, - null, - "WARNINGS.TRADE_BOX_TITLE.MARKET_STATUS_CLOSE_ONLY", - "WARNINGS.TRADE_BOX.MARKET_STATUS_CLOSE_ONLY", - mapOf( + errorDeprecated( + type = "ERROR", + errorCode = "CLOSED_MARKET", + fields = null, + actionStringKey = null, + titleStringKey = "WARNINGS.TRADE_BOX_TITLE.MARKET_STATUS_CLOSE_ONLY", + textStringKey = "WARNINGS.TRADE_BOX.MARKET_STATUS_CLOSE_ONLY", + textParams = mapOf( "MARKET" to mapOf( "value" to marketId, "format" to "string", diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/TriggerOrdersInputValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/TriggerOrdersInputValidator.kt index 1ffad7109..162663eb8 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/TriggerOrdersInputValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/TriggerOrdersInputValidator.kt @@ -1,7 +1,9 @@ package exchange.dydx.abacus.validator import abs +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.ValidationError import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.app.helper.Formatter @@ -24,12 +26,19 @@ internal class TriggerOrdersInputValidator( localizer: LocalizerProtocol?, formatter: Formatter?, parser: ParserProtocol -) : - BaseInputValidator(localizer, formatter, parser), ValidatorProtocol { +) : BaseInputValidator(localizer, formatter, parser), ValidatorProtocol { override fun validate( - staticTyping: Boolean, internalState: InternalState, + subaccountNumber: Int?, + currentBlockAndHeight: BlockAndTime?, + inputType: InputType, + environment: V4Environment?, + ): List? { + TODO("Not yet implemented") + } + + override fun validateDeprecated( wallet: Map?, user: Map?, subaccount: Map?, @@ -221,14 +230,14 @@ internal class TriggerOrdersInputValidator( tickSize: String, ): List? { return listOf( - error( - "ERROR", - errorCode, - listOf(TriggerOrdersInputField.stopLossPrice.rawValue), - "APP.TRADE.MODIFY_TRIGGER_PRICE", - titleStringKey, - textStringKey, - mapOf( + errorDeprecated( + type = "ERROR", + errorCode = errorCode, + fields = listOf(TriggerOrdersInputField.stopLossPrice.rawValue), + actionStringKey = "APP.TRADE.MODIFY_TRIGGER_PRICE", + titleStringKey = titleStringKey, + textStringKey = textStringKey, + textParams = mapOf( "TRIGGER_PRICE_LIMIT" to mapOf( "value" to liquidationPrice, "format" to "price", @@ -249,7 +258,7 @@ internal class TriggerOrdersInputValidator( if (triggerPrice == null && limitPrice != null) { errors.add( - required( + requiredDeprecated( "REQUIRED_TRIGGER_PRICE", "price.triggerPrice", "APP.TRADE.ENTER_TRIGGER_PRICE", @@ -291,13 +300,13 @@ internal class TriggerOrdersInputValidator( if (triggerPrice != null && triggerPrice <= 0 || (limitPrice != null && limitPrice <= 0)) { return listOf( - error( - "ERROR", - "PRICE_MUST_POSITIVE", - fields, - "APP.TRADE.MODIFY_PRICE", - "ERRORS.TRIGGERS_FORM_TITLE.PRICE_MUST_POSITIVE", - "ERRORS.TRIGGERS_FORM.PRICE_MUST_POSITIVE", + errorDeprecated( + type = "ERROR", + errorCode = "PRICE_MUST_POSITIVE", + fields = fields, + actionStringKey = "APP.TRADE.MODIFY_PRICE", + titleStringKey = "ERRORS.TRIGGERS_FORM_TITLE.PRICE_MUST_POSITIVE", + textStringKey = "ERRORS.TRIGGERS_FORM.PRICE_MUST_POSITIVE", ), ) } @@ -428,13 +437,13 @@ internal class TriggerOrdersInputValidator( textStringKey: String, ): List? { return listOf( - error( - "ERROR", - errorCode, - fields, - "APP.TRADE.MODIFY_TRIGGER_PRICE", - titleStringKey, - textStringKey, + errorDeprecated( + type = "ERROR", + errorCode = errorCode, + fields = fields, + actionStringKey = "APP.TRADE.MODIFY_TRIGGER_PRICE", + titleStringKey = titleStringKey, + textStringKey = textStringKey, ), ) } @@ -452,14 +461,14 @@ internal class TriggerOrdersInputValidator( parser.asDouble(configs["minOrderSize"])?.let { minOrderSize -> if (size.abs() < minOrderSize) { errors.add( - error( - "ERROR", - "ORDER_SIZE_BELOW_MIN_SIZE", - listOf(TriggerOrdersInputField.size.rawValue), - null, - "ERRORS.TRADE_BOX_TITLE.ORDER_SIZE_BELOW_MIN_SIZE", - "ERRORS.TRADE_BOX.ORDER_SIZE_BELOW_MIN_SIZE", - mapOf( + errorDeprecated( + type = "ERROR", + errorCode = "ORDER_SIZE_BELOW_MIN_SIZE", + fields = listOf(TriggerOrdersInputField.size.rawValue), + actionStringKey = null, + titleStringKey = "ERRORS.TRADE_BOX_TITLE.ORDER_SIZE_BELOW_MIN_SIZE", + textStringKey = "ERRORS.TRADE_BOX.ORDER_SIZE_BELOW_MIN_SIZE", + textParams = mapOf( "MIN_SIZE" to mapOf( "value" to minOrderSize, "format" to "size", @@ -499,40 +508,40 @@ internal class TriggerOrdersInputValidator( val isStopLoss = type == OrderType.StopLimit || type == OrderType.StopMarket return when (triggerToIndex) { - RelativeToPrice.ABOVE -> error( - "ERROR", - "TRIGGER_MUST_ABOVE_INDEX_PRICE", - fields, - action, - if (isStopLoss) { + RelativeToPrice.ABOVE -> errorDeprecated( + type = "ERROR", + errorCode = "TRIGGER_MUST_ABOVE_INDEX_PRICE", + fields = fields, + actionStringKey = action, + titleStringKey = if (isStopLoss) { "ERRORS.TRIGGERS_FORM_TITLE.STOP_LOSS_TRIGGER_MUST_ABOVE_INDEX_PRICE" } else { "ERRORS.TRIGGERS_FORM_TITLE.TAKE_PROFIT_TRIGGER_MUST_ABOVE_INDEX_PRICE" }, - if (isStopLoss) { + textStringKey = if (isStopLoss) { "ERRORS.TRIGGERS_FORM.STOP_LOSS_TRIGGER_MUST_ABOVE_INDEX_PRICE" } else { "ERRORS.TRIGGERS_FORM.TAKE_PROFIT_TRIGGER_MUST_ABOVE_INDEX_PRICE" }, - params, + textParams = params, ) - else -> error( - "ERROR", - "TRIGGER_MUST_BELOW_INDEX_PRICE", - fields, - action, - if (isStopLoss) { + else -> errorDeprecated( + type = "ERROR", + errorCode = "TRIGGER_MUST_BELOW_INDEX_PRICE", + fields = fields, + actionStringKey = action, + titleStringKey = if (isStopLoss) { "ERRORS.TRIGGERS_FORM_TITLE.STOP_LOSS_TRIGGER_MUST_BELOW_INDEX_PRICE" } else { "ERRORS.TRIGGERS_FORM_TITLE.TAKE_PROFIT_TRIGGER_MUST_BELOW_INDEX_PRICE" }, - if (isStopLoss) { + textStringKey = if (isStopLoss) { "ERRORS.TRIGGERS_FORM.STOP_LOSS_TRIGGER_MUST_BELOW_INDEX_PRICE" } else { "ERRORS.TRIGGERS_FORM.TAKE_PROFIT_TRIGGER_MUST_BELOW_INDEX_PRICE" }, - params, + textParams = params, ) } } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/ValidatorProtocols.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/ValidatorProtocols.kt index 9d3ffeeb1..67809bf6a 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/ValidatorProtocols.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/ValidatorProtocols.kt @@ -1,5 +1,7 @@ package exchange.dydx.abacus.validator +import exchange.dydx.abacus.output.input.InputType +import exchange.dydx.abacus.output.input.ValidationError import exchange.dydx.abacus.state.internalstate.InternalState import exchange.dydx.abacus.state.manager.BlockAndTime import exchange.dydx.abacus.state.manager.V4Environment @@ -24,8 +26,14 @@ enum class PositionChange(val rawValue: String) { internal interface ValidatorProtocol { fun validate( - staticTyping: Boolean, internalState: InternalState, + subaccountNumber: Int?, + currentBlockAndHeight: BlockAndTime?, + inputType: InputType, + environment: V4Environment?, + ): List? + + fun validateDeprecated( wallet: Map?, user: Map?, subaccount: Map?, @@ -40,8 +48,13 @@ internal interface ValidatorProtocol { internal interface TradeValidatorProtocol { fun validateTrade( - staticTyping: Boolean, internalState: InternalState, + change: PositionChange, + restricted: Boolean, + environment: V4Environment?, + ): List? + + fun validateTradeDeprecated( subaccount: Map?, market: Map?, configs: Map?, @@ -54,8 +67,13 @@ internal interface TradeValidatorProtocol { internal interface TransferValidatorProtocol { fun validateTransfer( - staticTyping: Boolean, internalState: InternalState, + currentBlockAndHeight: BlockAndTime?, + restricted: Boolean, + environment: V4Environment?, + ): List? + + fun validateTransferDeprecated( wallet: Map?, subaccount: Map?, transfer: Map, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeAccountStateValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeAccountStateValidator.kt index e2fb946a0..e4feb24fc 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeAccountStateValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeAccountStateValidator.kt @@ -1,5 +1,6 @@ package exchange.dydx.abacus.validator.trade +import exchange.dydx.abacus.output.input.ValidationError import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.app.helper.Formatter @@ -15,11 +16,17 @@ internal class TradeAccountStateValidator( localizer: LocalizerProtocol?, formatter: Formatter?, parser: ParserProtocol, -) : - BaseInputValidator(localizer, formatter, parser), TradeValidatorProtocol { +) : BaseInputValidator(localizer, formatter, parser), TradeValidatorProtocol { override fun validateTrade( - staticTyping: Boolean, internalState: InternalState, + change: PositionChange, + restricted: Boolean, + environment: V4Environment? + ): List? { + return null + } + + override fun validateTradeDeprecated( subaccount: Map?, market: Map?, configs: Map?, @@ -110,13 +117,13 @@ internal class TradeAccountStateValidator( marginUsage > Numeric.double.ONE ) ) { - error( - "ERROR", - "INVALID_NEW_ACCOUNT_MARGIN_USAGE", - listOf("size.size"), - "APP.TRADE.MODIFY_SIZE_FIELD", - "ERRORS.TRADE_BOX_TITLE.INVALID_NEW_ACCOUNT_MARGIN_USAGE", - "ERRORS.TRADE_BOX.INVALID_NEW_ACCOUNT_MARGIN_USAGE", + errorDeprecated( + type = "ERROR", + errorCode = "INVALID_NEW_ACCOUNT_MARGIN_USAGE", + fields = listOf("size.size"), + actionStringKey = "APP.TRADE.MODIFY_SIZE_FIELD", + titleStringKey = "ERRORS.TRADE_BOX_TITLE.INVALID_NEW_ACCOUNT_MARGIN_USAGE", + textStringKey = "ERRORS.TRADE_BOX.INVALID_NEW_ACCOUNT_MARGIN_USAGE", ) } else { null @@ -139,13 +146,13 @@ internal class TradeAccountStateValidator( parser.asNativeMap(subaccount["orders"]), ) ) { - error( - "ERROR", - "ORDER_CROSSES_OWN_ORDER", - listOf("size.size"), - "APP.TRADE.MODIFY_SIZE_FIELD", - "ERRORS.TRADE_BOX_TITLE.ORDER_CROSSES_OWN_ORDER", - "ERRORS.TRADE_BOX.ORDER_CROSSES_OWN_ORDER", + errorDeprecated( + type = "ERROR", + errorCode = "ORDER_CROSSES_OWN_ORDER", + fields = listOf("size.size"), + actionStringKey = "APP.TRADE.MODIFY_SIZE_FIELD", + titleStringKey = "ERRORS.TRADE_BOX_TITLE.ORDER_CROSSES_OWN_ORDER", + textStringKey = "ERRORS.TRADE_BOX.ORDER_CROSSES_OWN_ORDER", ) } else { null @@ -244,13 +251,13 @@ internal class TradeAccountStateValidator( } } if (overleveraged) { - error( - "ERROR", - "ORDER_WITH_CURRENT_ORDERS_INVALID", - null, - null, - "ERRORS.TRADE_BOX_TITLE.ORDER_WITH_CURRENT_ORDERS_INVALID", - "ERRORS.TRADE_BOX.ORDER_WITH_CURRENT_ORDERS_INVALID", + errorDeprecated( + type = "ERROR", + errorCode = "ORDER_WITH_CURRENT_ORDERS_INVALID", + fields = null, + actionStringKey = null, + titleStringKey = "ERRORS.TRADE_BOX_TITLE.ORDER_WITH_CURRENT_ORDERS_INVALID", + textStringKey = "ERRORS.TRADE_BOX.ORDER_WITH_CURRENT_ORDERS_INVALID", ) } else { null 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 6134ca1d6..b8a431de2 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeBracketOrdersValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeBracketOrdersValidator.kt @@ -1,5 +1,6 @@ package exchange.dydx.abacus.validator.trade +import exchange.dydx.abacus.output.input.ValidationError import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.app.helper.Formatter @@ -14,11 +15,17 @@ internal class TradeBracketOrdersValidator( localizer: LocalizerProtocol?, formatter: Formatter?, parser: ParserProtocol, -) : - BaseInputValidator(localizer, formatter, parser), TradeValidatorProtocol { +) : BaseInputValidator(localizer, formatter, parser), TradeValidatorProtocol { override fun validateTrade( - staticTyping: Boolean, internalState: InternalState, + change: PositionChange, + restricted: Boolean, + environment: V4Environment? + ): List? { + return null + } + + override fun validateTradeDeprecated( subaccount: Map?, market: Map?, configs: Map?, @@ -408,27 +415,27 @@ internal class TradeBracketOrdersValidator( text: String, params: Map?, ): Map { - return error( - "ERROR", - errorCode, - fields, - "APP.TRADE.ENTER_TRIGGER_PRICE", - title, - text, - params, + return errorDeprecated( + type = "ERROR", + errorCode = errorCode, + fields = fields, + actionStringKey = "APP.TRADE.ENTER_TRIGGER_PRICE", + titleStringKey = title, + textStringKey = text, + textParams = params, ) } private fun reduceOnlyError( field: List, ): Map { - return error( - "ERROR", - "WOULD_NOT_REDUCE_UNCHECK", - field, - "APP.TRADE.ENTER_TRIGGER_PRICE", - "ERRORS.TRADE_BOX_TITLE.WOULD_NOT_REDUCE_UNCHECK", - "ERRORS.TRADE_BOX.WOULD_NOT_REDUCE_UNCHECK", + return errorDeprecated( + type = "ERROR", + errorCode = "WOULD_NOT_REDUCE_UNCHECK", + fields = field, + actionStringKey = "APP.TRADE.ENTER_TRIGGER_PRICE", + titleStringKey = "ERRORS.TRADE_BOX_TITLE.WOULD_NOT_REDUCE_UNCHECK", + textStringKey = "ERRORS.TRADE_BOX.WOULD_NOT_REDUCE_UNCHECK", ) } } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeInputDataValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeInputDataValidator.kt index 361ab0192..68360fcde 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeInputDataValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeInputDataValidator.kt @@ -1,6 +1,7 @@ package exchange.dydx.abacus.validator.trade import abs +import exchange.dydx.abacus.output.input.ValidationError import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.app.helper.Formatter @@ -31,12 +32,18 @@ internal class TradeInputDataValidator( localizer: LocalizerProtocol?, formatter: Formatter?, parser: ParserProtocol, -) : - BaseInputValidator(localizer, formatter, parser), TradeValidatorProtocol { +) : BaseInputValidator(localizer, formatter, parser), TradeValidatorProtocol { override fun validateTrade( - staticTyping: Boolean, internalState: InternalState, + change: PositionChange, + restricted: Boolean, + environment: V4Environment? + ): List? { + return null + } + + override fun validateTradeDeprecated( subaccount: Map?, market: Map?, configs: Map?, @@ -96,14 +103,14 @@ internal class TradeInputDataValidator( parser.asDouble(configs["minOrderSize"])?.let { minOrderSize -> if (size.abs() < minOrderSize) { errors.add( - error( - "ERROR", - "ORDER_SIZE_BELOW_MIN_SIZE", - null, - null, - "ERRORS.TRADE_BOX_TITLE.ORDER_SIZE_BELOW_MIN_SIZE", - "ERRORS.TRADE_BOX.ORDER_SIZE_BELOW_MIN_SIZE", - mapOf( + errorDeprecated( + type = "ERROR", + errorCode = "ORDER_SIZE_BELOW_MIN_SIZE", + fields = null, + actionStringKey = null, + titleStringKey = "ERRORS.TRADE_BOX_TITLE.ORDER_SIZE_BELOW_MIN_SIZE", + textStringKey = "ERRORS.TRADE_BOX.ORDER_SIZE_BELOW_MIN_SIZE", + textParams = mapOf( "MIN_SIZE" to mapOf( "value" to minOrderSize, "format" to "size", @@ -143,25 +150,25 @@ internal class TradeInputDataValidator( if (side == "BUY" && limitPrice < triggerPrice) { // BUY return listOf( - error( - "ERROR", - "LIMIT_MUST_ABOVE_TRIGGER_PRICE", - listOf("price.triggerPrice"), - "APP.TRADE.MODIFY_TRIGGER_PRICE", - "ERRORS.TRADE_BOX_TITLE.LIMIT_MUST_ABOVE_TRIGGER_PRICE", - "ERRORS.TRADE_BOX.LIMIT_MUST_ABOVE_TRIGGER_PRICE", + errorDeprecated( + type = "ERROR", + errorCode = "LIMIT_MUST_ABOVE_TRIGGER_PRICE", + fields = listOf("price.triggerPrice"), + actionStringKey = "APP.TRADE.MODIFY_TRIGGER_PRICE", + titleStringKey = "ERRORS.TRADE_BOX_TITLE.LIMIT_MUST_ABOVE_TRIGGER_PRICE", + textStringKey = "ERRORS.TRADE_BOX.LIMIT_MUST_ABOVE_TRIGGER_PRICE", ), ) } else if (side == "SELL" && limitPrice > triggerPrice) { // SELL return listOf( - error( - "ERROR", - "LIMIT_MUST_BELOW_TRIGGER_PRICE", - listOf("price.triggerPrice"), - "APP.TRADE.MODIFY_TRIGGER_PRICE", - "ERRORS.TRADE_BOX_TITLE.LIMIT_MUST_BELOW_TRIGGER_PRICE", - "ERRORS.TRADE_BOX.LIMIT_MUST_BELOW_TRIGGER_PRICE", + errorDeprecated( + type = "ERROR", + errorCode = "LIMIT_MUST_BELOW_TRIGGER_PRICE", + fields = listOf("price.triggerPrice"), + actionStringKey = "APP.TRADE.MODIFY_TRIGGER_PRICE", + titleStringKey = "ERRORS.TRADE_BOX_TITLE.LIMIT_MUST_BELOW_TRIGGER_PRICE", + textStringKey = "ERRORS.TRADE_BOX.LIMIT_MUST_BELOW_TRIGGER_PRICE", ), ) } else { @@ -200,13 +207,13 @@ internal class TradeInputDataValidator( val timeInterval = GoodTil.duration(goodTil, parser) if (timeInterval != null && timeInterval > 90.days) { listOf( - error( - "ERROR", - "INVALID_GOOD_TIL", - listOf("goodTil"), - "APP.TRADE.MODIFY_GOOD_TIL", - "ERRORS.TRADE_BOX_TITLE.INVALID_GOOD_TIL", - "ERRORS.TRADE_BOX.INVALID_GOOD_TIL_MAX_90_DAYS", + errorDeprecated( + type = "ERROR", + errorCode = "INVALID_GOOD_TIL", + fields = listOf("goodTil"), + actionStringKey = "APP.TRADE.MODIFY_GOOD_TIL", + titleStringKey = "ERRORS.TRADE_BOX_TITLE.INVALID_GOOD_TIL", + textStringKey = "ERRORS.TRADE_BOX.INVALID_GOOD_TIL_MAX_90_DAYS", ), ) } else { diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeMarketOrderInputValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeMarketOrderInputValidator.kt index 400de7db3..8e4cb09bf 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeMarketOrderInputValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeMarketOrderInputValidator.kt @@ -1,6 +1,7 @@ package exchange.dydx.abacus.validator.trade import abs +import exchange.dydx.abacus.output.input.ValidationError import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.app.helper.Formatter @@ -19,8 +20,15 @@ internal class TradeMarketOrderInputValidator( private val marketOrderWarningSlippage = 0.05 override fun validateTrade( - staticTyping: Boolean, internalState: InternalState, + change: PositionChange, + restricted: Boolean, + environment: V4Environment? + ): List? { + return null + } + + override fun validateTradeDeprecated( subaccount: Map?, market: Map?, configs: Map?, @@ -149,14 +157,14 @@ internal class TradeMarketOrderInputValidator( actionStringKey: String? = null, slippagePercentValue: Double? = null ): Map { - return error( + return errorDeprecated( type = errorLevel, - errorCode, - fields, - actionStringKey, - "ERRORS.TRADE_BOX_TITLE.$errorCode", - "ERRORS.TRADE_BOX.$errorCode", - slippagePercentValue?.let { + errorCode = errorCode, + fields = fields, + actionStringKey = actionStringKey, + titleStringKey = "ERRORS.TRADE_BOX_TITLE.$errorCode", + textStringKey = "ERRORS.TRADE_BOX.$errorCode", + textParams = slippagePercentValue?.let { mapOf( "SLIPPAGE" to mapOf( "value" to it, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradePositionStateValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradePositionStateValidator.kt index ebf31252d..51523cc36 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradePositionStateValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradePositionStateValidator.kt @@ -1,5 +1,6 @@ package exchange.dydx.abacus.validator.trade +import exchange.dydx.abacus.output.input.ValidationError import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.app.helper.Formatter @@ -14,11 +15,17 @@ internal class TradePositionStateValidator( localizer: LocalizerProtocol?, formatter: Formatter?, parser: ParserProtocol, -) : - BaseInputValidator(localizer, formatter, parser), TradeValidatorProtocol { +) : BaseInputValidator(localizer, formatter, parser), TradeValidatorProtocol { override fun validateTrade( - staticTyping: Boolean, internalState: InternalState, + change: PositionChange, + restricted: Boolean, + environment: V4Environment? + ): List? { + return null + } + + override fun validateTradeDeprecated( subaccount: Map?, market: Map?, configs: Map?, @@ -79,14 +86,14 @@ internal class TradePositionStateValidator( PositionChange.CROSSING, PositionChange.NEW, PositionChange.INCREASING -> true else -> false } - error( - if (isError) "ERROR" else "WARNING", - "MARKET_STATUS_CLOSE_ONLY", - if (isError) listOf("size.size") else null, - if (isError) "APP.TRADE.MODIFY_SIZE_FIELD" else null, - "WARNINGS.TRADE_BOX_TITLE.MARKET_STATUS_CLOSE_ONLY", - "WARNINGS.TRADE_BOX.MARKET_STATUS_CLOSE_ONLY", - mapOf( + errorDeprecated( + type = if (isError) "ERROR" else "WARNING", + errorCode = "MARKET_STATUS_CLOSE_ONLY", + fields = if (isError) listOf("size.size") else null, + actionStringKey = if (isError) "APP.TRADE.MODIFY_SIZE_FIELD" else null, + titleStringKey = "WARNINGS.TRADE_BOX_TITLE.MARKET_STATUS_CLOSE_ONLY", + textStringKey = "WARNINGS.TRADE_BOX.MARKET_STATUS_CLOSE_ONLY", + textParams = mapOf( "MARKET" to mapOf( "value" to marketId, "format" to "string", @@ -113,14 +120,14 @@ internal class TradePositionStateValidator( } val symbol = parser.asString(market?.get("assetId")) ?: return null return if (size > maxSize) { - error( - "ERROR", - "NEW_POSITION_SIZE_OVER_MAX", - listOf("size.size"), - "APP.TRADE.MODIFY_SIZE_FIELD", - "ERRORS.TRADE_BOX_TITLE.NEW_POSITION_SIZE_OVER_MAX", - "ERRORS.TRADE_BOX.NEW_POSITION_SIZE_OVER_MAX", - mapOf( + errorDeprecated( + type = "ERROR", + errorCode = "NEW_POSITION_SIZE_OVER_MAX", + fields = listOf("size.size"), + actionStringKey = "APP.TRADE.MODIFY_SIZE_FIELD", + titleStringKey = "ERRORS.TRADE_BOX_TITLE.NEW_POSITION_SIZE_OVER_MAX", + textStringKey = "ERRORS.TRADE_BOX.NEW_POSITION_SIZE_OVER_MAX", + textParams = mapOf( "MAX_SIZE" to mapOf( "value" to maxSize, "format" to "size", @@ -146,13 +153,13 @@ internal class TradePositionStateValidator( val needsReduceOnly = parser.asBool(parser.value(trade, "options.needsReduceOnly")) ?: false return if (needsReduceOnly && parser.asBool(trade["reduceOnly"]) == true) { when (change) { - PositionChange.NEW, PositionChange.INCREASING, PositionChange.CROSSING -> error( - "ERROR", - "ORDER_WOULD_FLIP_POSITION", - listOf("size.size"), - "APP.TRADE.MODIFY_SIZE_FIELD", - "ERRORS.TRADE_BOX_TITLE.ORDER_WOULD_FLIP_POSITION", - "ERRORS.TRADE_BOX.ORDER_WOULD_FLIP_POSITION", + PositionChange.NEW, PositionChange.INCREASING, PositionChange.CROSSING -> errorDeprecated( + type = "ERROR", + errorCode = "ORDER_WOULD_FLIP_POSITION", + fields = listOf("size.size"), + actionStringKey = "APP.TRADE.MODIFY_SIZE_FIELD", + titleStringKey = "ERRORS.TRADE_BOX_TITLE.ORDER_WOULD_FLIP_POSITION", + textStringKey = "ERRORS.TRADE_BOX.ORDER_WOULD_FLIP_POSITION", ) else -> null diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeResctrictedValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeResctrictedValidator.kt new file mode 100644 index 000000000..02aaa9318 --- /dev/null +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeResctrictedValidator.kt @@ -0,0 +1,111 @@ +package exchange.dydx.abacus.validator.trade + +import exchange.dydx.abacus.output.input.ValidationError +import exchange.dydx.abacus.protocols.LocalizerProtocol +import exchange.dydx.abacus.protocols.ParserProtocol +import exchange.dydx.abacus.state.app.helper.Formatter +import exchange.dydx.abacus.state.internalstate.InternalState +import exchange.dydx.abacus.state.manager.V4Environment +import exchange.dydx.abacus.validator.BaseInputValidator +import exchange.dydx.abacus.validator.PositionChange +import exchange.dydx.abacus.validator.TradeValidatorProtocol + +internal class TradeResctrictedValidator( + localizer: LocalizerProtocol?, + formatter: Formatter?, + parser: ParserProtocol, +) : BaseInputValidator(localizer, formatter, parser), TradeValidatorProtocol { + override fun validateTrade( + internalState: InternalState, + change: PositionChange, + restricted: Boolean, + environment: V4Environment? + ): List? { + return null + } + + override fun validateTradeDeprecated( + subaccount: Map?, + market: Map?, + configs: Map?, + trade: Map, + change: PositionChange, + restricted: Boolean, + environment: V4Environment?, + ): List? { + val closeOnlyError = + validateClosingOnlyDeprecated( + parser = parser, + market = market, + change = change, + restricted = restricted, + ) + + return closeOnlyError?.let { listOf(it) } + } + + private fun validateClosingOnlyDeprecated( + parser: ParserProtocol, + market: Map?, + change: PositionChange, + restricted: Boolean, + ): Map? { + val marketId = parser.asNativeMap(market?.get("assetId")) ?: "" + val canTrade = parser.asBool(parser.value(market, "status.canTrade")) ?: true + val canReduce = parser.asBool(parser.value(market, "status.canTrade")) ?: true + return if (canTrade) { + if (restricted) { + when (change) { + PositionChange.NEW, PositionChange.INCREASING, PositionChange.CROSSING -> + errorDeprecated( + type = "ERROR", + errorCode = "RESTRICTED_USER", + fields = null, + actionStringKey = null, + titleStringKey = "ERRORS.TRADE_BOX_TITLE.MARKET_ORDER_CLOSE_POSITION_ONLY", + textStringKey = "ERRORS.TRADE_BOX.MARKET_ORDER_CLOSE_POSITION_ONLY", + ) + + else -> null + } + } else { + return null + } + } else if (canReduce) { + when (change) { + PositionChange.NEW, PositionChange.INCREASING, PositionChange.CROSSING -> + errorDeprecated( + type = "ERROR", + errorCode = "CLOSE_ONLY_MARKET", + fields = listOf("size.size"), + actionStringKey = "APP.TRADE.MODIFY_SIZE_FIELD", + titleStringKey = "WARNINGS.TRADE_BOX_TITLE.MARKET_STATUS_CLOSE_ONLY", + textStringKey = "WARNINGS.TRADE_BOX.MARKET_STATUS_CLOSE_ONLY", + textParams = mapOf( + "MARKET" to mapOf( + "value" to marketId, + "format" to "string", + ), + ), + ) + + else -> null + } + } else { + errorDeprecated( + type = "ERROR", + errorCode = "CLOSED_MARKET", + fields = null, + actionStringKey = null, + titleStringKey = "WARNINGS.TRADE_BOX_TITLE.MARKET_STATUS_CLOSE_ONLY", + textStringKey = "WARNINGS.TRADE_BOX.MARKET_STATUS_CLOSE_ONLY", + textParams = mapOf( + "MARKET" to mapOf( + "value" to marketId, + "format" to "string", + ), + ), + ) + } + } +} diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeTriggerPriceValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeTriggerPriceValidator.kt index 7111f9407..863503475 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeTriggerPriceValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeTriggerPriceValidator.kt @@ -1,5 +1,6 @@ package exchange.dydx.abacus.validator.trade +import exchange.dydx.abacus.output.input.ValidationError import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.app.helper.Formatter @@ -24,6 +25,15 @@ internal class TradeTriggerPriceValidator( formatter: Formatter?, parser: ParserProtocol, ) : BaseInputValidator(localizer, formatter, parser), TradeValidatorProtocol { + override fun validateTrade( + internalState: InternalState, + change: PositionChange, + restricted: Boolean, + environment: V4Environment? + ): List? { + return null + } + /* They are still used to calculate payload, but no longer used for validation private val stopMarketSlippageBufferBTC = 0.05; // 5% for Stop Market @@ -32,9 +42,7 @@ internal class TradeTriggerPriceValidator( private val takeProfitMarketSlippageBuffer = 0.2; // 20% for Take Profit Market */ - override fun validateTrade( - staticTyping: Boolean, - internalState: InternalState, + override fun validateTradeDeprecated( subaccount: Map?, market: Map?, configs: Map?, @@ -178,24 +186,24 @@ internal class TradeTriggerPriceValidator( ), ) return when (triggerToIndex) { - RelativeToPrice.ABOVE -> error( - "ERROR", - "TRIGGER_MUST_ABOVE_INDEX_PRICE", - fields, - action, - "ERRORS.TRADE_BOX_TITLE.TRIGGER_MUST_ABOVE_INDEX_PRICE", - "ERRORS.TRADE_BOX.TRIGGER_MUST_ABOVE_INDEX_PRICE", - params, + RelativeToPrice.ABOVE -> errorDeprecated( + type = "ERROR", + errorCode = "TRIGGER_MUST_ABOVE_INDEX_PRICE", + fields = fields, + actionStringKey = action, + titleStringKey = "ERRORS.TRADE_BOX_TITLE.TRIGGER_MUST_ABOVE_INDEX_PRICE", + textStringKey = "ERRORS.TRADE_BOX.TRIGGER_MUST_ABOVE_INDEX_PRICE", + textParams = params, ) - else -> error( - "ERROR", - "TRIGGER_MUST_BELOW_INDEX_PRICE", - fields, - action, - "ERRORS.TRADE_BOX_TITLE.TRIGGER_MUST_BELOW_INDEX_PRICE", - "ERRORS.TRADE_BOX.TRIGGER_MUST_BELOW_INDEX_PRICE", - params, + else -> errorDeprecated( + type = "ERROR", + errorCode = "TRIGGER_MUST_BELOW_INDEX_PRICE", + fields = fields, + actionStringKey = action, + titleStringKey = "ERRORS.TRADE_BOX_TITLE.TRIGGER_MUST_BELOW_INDEX_PRICE", + textStringKey = "ERRORS.TRADE_BOX.TRIGGER_MUST_BELOW_INDEX_PRICE", + textParams = params, ) } } @@ -253,24 +261,24 @@ internal class TradeTriggerPriceValidator( ), ) return when (triggerToLiquidation) { - RelativeToPrice.ABOVE -> error( - "ERROR", - "SELL_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE", - fields, - action, - "ERRORS.TRADE_BOX_TITLE.SELL_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE", - "ERRORS.TRADE_BOX.SELL_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE", - params, + RelativeToPrice.ABOVE -> errorDeprecated( + type = "ERROR", + errorCode = "SELL_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE", + fields = fields, + actionStringKey = action, + titleStringKey = "ERRORS.TRADE_BOX_TITLE.SELL_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE", + textStringKey = "ERRORS.TRADE_BOX.SELL_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE", + textParams = params, ) - RelativeToPrice.BELOW -> error( - "ERROR", - "BUY_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE", - fields, - action, - "ERRORS.TRADE_BOX_TITLE.BUY_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE", - "ERRORS.TRADE_BOX.BUY_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE", - params, + RelativeToPrice.BELOW -> errorDeprecated( + type = "ERROR", + errorCode = "BUY_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE", + fields = fields, + actionStringKey = action, + titleStringKey = "ERRORS.TRADE_BOX_TITLE.BUY_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE", + textStringKey = "ERRORS.TRADE_BOX.BUY_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE", + textParams = params, ) } } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/transfer/DepositValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/transfer/DepositValidator.kt index 19c203283..fc5411f10 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/transfer/DepositValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/transfer/DepositValidator.kt @@ -1,5 +1,6 @@ package exchange.dydx.abacus.validator.transfer +import exchange.dydx.abacus.output.input.ValidationError import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.app.helper.Formatter @@ -15,8 +16,15 @@ internal class DepositValidator( parser: ParserProtocol, ) : BaseInputValidator(localizer, formatter, parser), TransferValidatorProtocol { override fun validateTransfer( - staticTyping: Boolean, internalState: InternalState, + currentBlockAndHeight: BlockAndTime?, + restricted: Boolean, + environment: V4Environment? + ): List? { + return null + } + + override fun validateTransferDeprecated( wallet: Map?, subaccount: Map?, transfer: Map, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/transfer/TransferOutValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/transfer/TransferOutValidator.kt index 871ed95d8..87b9f8164 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/transfer/TransferOutValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/transfer/TransferOutValidator.kt @@ -1,5 +1,6 @@ package exchange.dydx.abacus.validator.transfer +import exchange.dydx.abacus.output.input.ValidationError import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.app.helper.Formatter @@ -16,8 +17,15 @@ internal class TransferOutValidator( parser: ParserProtocol, ) : BaseInputValidator(localizer, formatter, parser), TransferValidatorProtocol { override fun validateTransfer( - staticTyping: Boolean, internalState: InternalState, + currentBlockAndHeight: BlockAndTime?, + restricted: Boolean, + environment: V4Environment? + ): List? { + return null + } + + override fun validateTransferDeprecated( wallet: Map?, subaccount: Map?, transfer: Map, @@ -30,13 +38,13 @@ internal class TransferOutValidator( val type = parser.asString(parser.value(transfer, "type")) if (type == "TRANSFER_OUT" && !address.isNullOrEmpty() && !address.isAddressValid()) { return listOf( - error( - "ERROR", - "INVALID_ADDRESS", - listOf("address"), - "APP.DIRECT_TRANSFER_MODAL.ADDRESS_FIELD", - "APP.DIRECT_TRANSFER_MODAL.INVALID_ADDRESS_TITLE", - "APP.DIRECT_TRANSFER_MODAL.INVALID_ADDRESS_BODY", + errorDeprecated( + type = "ERROR", + errorCode = "INVALID_ADDRESS", + fields = listOf("address"), + actionStringKey = "APP.DIRECT_TRANSFER_MODAL.ADDRESS_FIELD", + titleStringKey = "APP.DIRECT_TRANSFER_MODAL.INVALID_ADDRESS_TITLE", + textStringKey = "APP.DIRECT_TRANSFER_MODAL.INVALID_ADDRESS_BODY", ), ) } else { 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 f10d9d414..455cb0615 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/transfer/WithdrawalCapacityValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/transfer/WithdrawalCapacityValidator.kt @@ -3,6 +3,7 @@ package exchange.dydx.abacus.validator.transfer import com.ionspin.kotlin.bignum.decimal.BigDecimal import exchange.dydx.abacus.output.input.ErrorType import exchange.dydx.abacus.output.input.TransferType +import exchange.dydx.abacus.output.input.ValidationError import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.app.helper.Formatter @@ -18,8 +19,15 @@ internal class WithdrawalCapacityValidator( parser: ParserProtocol, ) : BaseInputValidator(localizer, formatter, parser), TransferValidatorProtocol { override fun validateTransfer( - staticTyping: Boolean, internalState: InternalState, + currentBlockAndHeight: BlockAndTime?, + restricted: Boolean, + environment: V4Environment? + ): List? { + TODO("Not yet implemented") + } + + override fun validateTransferDeprecated( wallet: Map?, subaccount: Map?, transfer: Map, @@ -29,11 +37,12 @@ internal class WithdrawalCapacityValidator( environment: V4Environment? ): List? { val withdrawalCapacity = parser.asMap(parser.value(configs, "withdrawalCapacity")) - val maxWithdrawalCapacity = if (staticTyping) { - internalState.configs.withdrawalCapacity?.maxWithdrawalCapacity ?: BigDecimal.fromLong(Long.MAX_VALUE) - } else { - parser.asDecimal(parser.value(withdrawalCapacity, "maxWithdrawalCapacity")) ?: BigDecimal.fromLong(Long.MAX_VALUE) - } +// val maxWithdrawalCapacity = if (staticTyping) { +// internalState.configs.withdrawalCapacity?.maxWithdrawalCapacity ?: BigDecimal.fromLong(Long.MAX_VALUE) +// } else { +// parser.asDecimal(parser.value(withdrawalCapacity, "maxWithdrawalCapacity")) ?: BigDecimal.fromLong(Long.MAX_VALUE) +// } + val maxWithdrawalCapacity = parser.asDecimal(parser.value(withdrawalCapacity, "maxWithdrawalCapacity")) ?: BigDecimal.fromLong(Long.MAX_VALUE) val type = parser.asString(parser.value(transfer, "type")) val size = parser.asMap(parser.value(transfer, "size")) val usdcSize = parser.asDecimal(size?.get("usdcSize")) ?: BigDecimal.ZERO @@ -41,7 +50,7 @@ internal class WithdrawalCapacityValidator( if (type == TransferType.withdrawal.rawValue && usdcSizeInputIsGreaterThanCapacity) { return listOf( - error( + errorDeprecated( type = ErrorType.error.rawValue, errorCode = "", fields = null, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/transfer/WithdrawalGatingValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/transfer/WithdrawalGatingValidator.kt index e81f33182..5232ab708 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/transfer/WithdrawalGatingValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/transfer/WithdrawalGatingValidator.kt @@ -2,6 +2,7 @@ package exchange.dydx.abacus.validator.transfer import exchange.dydx.abacus.output.input.ErrorType import exchange.dydx.abacus.output.input.TransferType +import exchange.dydx.abacus.output.input.ValidationError import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.app.helper.Formatter @@ -17,8 +18,15 @@ internal class WithdrawalGatingValidator( parser: ParserProtocol, ) : BaseInputValidator(localizer, formatter, parser), TransferValidatorProtocol { override fun validateTransfer( - staticTyping: Boolean, internalState: InternalState, + currentBlockAndHeight: BlockAndTime?, + restricted: Boolean, + environment: V4Environment? + ): List? { + return null + } + + override fun validateTransferDeprecated( wallet: Map?, subaccount: Map?, transfer: Map, @@ -29,11 +37,12 @@ internal class WithdrawalGatingValidator( ): List? { val currentBlock = currentBlockAndHeight?.block ?: Int.MAX_VALUE // parser.asInt(parser.value(environment, "currentBlock")) val withdrawalGating = parser.asMap(parser.value(configs, "withdrawalGating")) - val withdrawalsAndTransfersUnblockedAtBlock = if (staticTyping) { - internalState.configs.withdrawalGating?.withdrawalsAndTransfersUnblockedAtBlock ?: 0 - } else { - parser.asInt(withdrawalGating?.get("withdrawalsAndTransfersUnblockedAtBlock")) ?: 0 - } +// val withdrawalsAndTransfersUnblockedAtBlock = if (staticTyping) { +// internalState.configs.withdrawalGating?.withdrawalsAndTransfersUnblockedAtBlock ?: 0 +// } else { +// parser.asInt(withdrawalGating?.get("withdrawalsAndTransfersUnblockedAtBlock")) ?: 0 +// } + val withdrawalsAndTransfersUnblockedAtBlock = parser.asInt(withdrawalGating?.get("withdrawalsAndTransfersUnblockedAtBlock")) ?: 0 val blockDurationSeconds = if (environment?.isMainNet == true) 1.1 else 1.5 val secondsUntilUnblock = ((withdrawalsAndTransfersUnblockedAtBlock - currentBlock) * blockDurationSeconds).toInt() @@ -43,7 +52,7 @@ internal class WithdrawalGatingValidator( secondsUntilUnblock > 0 ) { return listOf( - error( + errorDeprecated( type = ErrorType.error.rawValue, errorCode = "", fields = null, From d50b093c88f0bb837cfb71fafb7700b44e030054 Mon Sep 17 00:00:00 2001 From: Rui Date: Mon, 19 Aug 2024 07:25:58 +0900 Subject: [PATCH 47/63] TradeInputDataValidator --- .../output/input/TradeInput.kt | 18 +++ .../state/internalstate/InternalState.kt | 2 + .../SubaccountTransactionPayloadProvider.kt | 19 +-- .../exchange.dydx.abacus/utils/GoodTil.kt | 14 -- .../validator/TradeInputValidator.kt | 59 +++++++- .../trade/TradeInputDataValidator.kt | 139 +++++++++++++++++- .../trade/TradeResctrictedValidator.kt | 80 +++++++++- 7 files changed, 297 insertions(+), 34 deletions(-) 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 f6a969d8f..2cd8ad949 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/output/input/TradeInput.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/TradeInput.kt @@ -12,6 +12,10 @@ import kollections.iListOf import kollections.iMutableListOf import kollections.toIList import kotlinx.serialization.Serializable +import kotlin.time.Duration +import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.minutes @JsExport @Serializable @@ -595,6 +599,20 @@ data class TradeInputGoodUntil( val duration: Double?, val unit: String?, ) { + internal val timeInterval: Duration? + get() = + if (duration != null && unit != null) { + when (unit) { + "M" -> duration.minutes + "H" -> duration.hours + "D" -> duration.days + "W" -> (duration * 7).days + else -> null + } + } else { + null + } + companion object { internal fun create( existing: TradeInputGoodUntil?, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt index a2e1b4cc8..93bb8cc28 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt @@ -191,6 +191,8 @@ internal data class InternalUserState( var takerFeeRate: Double? = null, var makerVolume30D: Double? = null, var takerVolume30D: Double? = null, + + var restricted: Boolean = false, // TODO: Not being used ) internal data class InternalAccountState( diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/SubaccountTransactionPayloadProvider.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/SubaccountTransactionPayloadProvider.kt index 38e5a126b..a9699734c 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/SubaccountTransactionPayloadProvider.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/v2/supervisor/SubaccountTransactionPayloadProvider.kt @@ -18,7 +18,6 @@ import exchange.dydx.abacus.state.manager.HumanReadableTriggerOrdersPayload import exchange.dydx.abacus.state.manager.HumanReadableWithdrawPayload import exchange.dydx.abacus.state.manager.PlaceOrderMarketInfo import exchange.dydx.abacus.state.model.TradingStateMachine -import exchange.dydx.abacus.utils.GoodTil import exchange.dydx.abacus.utils.MAX_SUBACCOUNT_NUMBER import exchange.dydx.abacus.utils.NUM_PARENT_SUBACCOUNTS import exchange.dydx.abacus.utils.SHORT_TERM_ORDER_DURATION @@ -97,16 +96,12 @@ internal class SubaccountTransactionPayloadProvider( } val goodTilTimeInSeconds = ( - ( - if (trade.options?.goodTilUnitOptions != null) { - val timeInterval = - GoodTil.duration(trade.goodTil) - ?: throw Exception("goodTil is null") - timeInterval / 1.seconds - } else { - null - } - ) + if (trade.options?.goodTilUnitOptions != null) { + val timeInterval = trade.goodTil?.timeInterval ?: throw Exception("goodTil is null") + timeInterval / 1.seconds + } else { + null + } )?.toInt() val goodTilBlock = @@ -380,7 +375,7 @@ internal class SubaccountTransactionPayloadProvider( else -> error("invalid triggerOrderType") } - val duration = GoodTil.duration(TradeInputGoodUntil(TRIGGER_ORDER_DEFAULT_DURATION_DAYS, "D")) ?: throw Exception("invalid duration") + val duration = TradeInputGoodUntil(TRIGGER_ORDER_DEFAULT_DURATION_DAYS, "D").timeInterval ?: throw Exception("invalid duration") val goodTilTimeInSeconds = (duration / 1.seconds).toInt() val goodTilBlock = null diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/utils/GoodTil.kt b/src/commonMain/kotlin/exchange.dydx.abacus/utils/GoodTil.kt index d6c8a812d..40b8a393e 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/utils/GoodTil.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/utils/GoodTil.kt @@ -1,6 +1,5 @@ package exchange.dydx.abacus.utils -import exchange.dydx.abacus.output.input.TradeInputGoodUntil import exchange.dydx.abacus.protocols.ParserProtocol import kotlin.time.Duration import kotlin.time.Duration.Companion.days @@ -21,18 +20,5 @@ class GoodTil { } return timeInterval } - - internal fun duration(goodTil: TradeInputGoodUntil?): Duration? { - if (goodTil === null) return null - val duration = goodTil.duration ?: return null - val timeInterval = when (goodTil.unit) { - "M" -> duration.minutes - "H" -> duration.hours - "D" -> duration.days - "W" -> (duration * 7).days - else -> return null - } - return timeInterval - } } } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/TradeInputValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/TradeInputValidator.kt index 9be81192c..64b67daca 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/TradeInputValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/TradeInputValidator.kt @@ -1,11 +1,14 @@ package exchange.dydx.abacus.validator +import exchange.dydx.abacus.calculator.CalculationPeriod import exchange.dydx.abacus.output.input.InputType import exchange.dydx.abacus.output.input.ValidationError import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.app.helper.Formatter import exchange.dydx.abacus.state.internalstate.InternalState +import exchange.dydx.abacus.state.internalstate.InternalSubaccountState +import exchange.dydx.abacus.state.internalstate.InternalTradeInputState import exchange.dydx.abacus.state.manager.BlockAndTime import exchange.dydx.abacus.state.manager.V4Environment import exchange.dydx.abacus.utils.Numeric @@ -43,14 +46,19 @@ internal class TradeInputValidator( if (transactionType != InputType.TRANSFER || transactionType != InputType.CLOSE_POSITION) { return null } + val change = getPositionChange( + subaccount = internalState.wallet.account.subaccounts[subaccountNumber], + trade = internalState.input.trade, + ) + val restricted = internalState.wallet.user?.restricted ?: false val errors = mutableListOf() for (validator in tradeValidators) { val validatorErrors = validator.validateTrade( internalState = internalState, - change = PositionChange.NONE, - restricted = false, + change = change, + restricted = restricted, environment = environment, ) if (validatorErrors != null) { @@ -74,7 +82,7 @@ internal class TradeInputValidator( ): List? { if (transactionType == "trade" || transactionType == "closePosition") { val marketId = parser.asString(transaction["marketId"]) ?: return null - val change = change(parser, subaccount, transaction) + val change = getPositionChangeDeprecated(parser, subaccount, transaction) val restricted = parser.asBool(user?.get("restricted")) ?: false val market = parser.asNativeMap(markets?.get(marketId)) val errors = mutableListOf() @@ -99,7 +107,50 @@ internal class TradeInputValidator( return null } - private fun change( + private fun getPositionChange( + subaccount: InternalSubaccountState?, + trade: InternalTradeInputState, + ): PositionChange { + val marketId = trade.marketId ?: return PositionChange.NONE + val position = subaccount?.openPositions?.get(marketId) ?: return PositionChange.NONE + val size = position.calculated[CalculationPeriod.current]?.size ?: Numeric.double.ZERO + val postOrder = position.calculated[CalculationPeriod.post]?.size ?: Numeric.double.ZERO + return if (size != Numeric.double.ZERO) { + if (postOrder != Numeric.double.ZERO) { + if (size > Numeric.double.ZERO) { + if (postOrder > size) { + PositionChange.INCREASING + } else if (postOrder < Numeric.double.ZERO) { + PositionChange.CROSSING + } else if (postOrder < size) { + PositionChange.DECREASING + } else { + PositionChange.NONE + } + } else { + if (postOrder > size) { + PositionChange.DECREASING + } else if (postOrder > Numeric.double.ZERO) { + PositionChange.CROSSING + } else if (postOrder < size) { + PositionChange.INCREASING + } else { + PositionChange.NONE + } + } + } else { + PositionChange.CLOSING + } + } else { + if (postOrder != Numeric.double.ZERO) { + PositionChange.NEW + } else { + PositionChange.NONE + } + } + } + + private fun getPositionChangeDeprecated( parser: ParserProtocol, subaccount: Map?, trade: Map, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeInputDataValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeInputDataValidator.kt index 68360fcde..cbfc90281 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeInputDataValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeInputDataValidator.kt @@ -1,11 +1,16 @@ package exchange.dydx.abacus.validator.trade import abs +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.output.input.ValidationError import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.app.helper.Formatter +import exchange.dydx.abacus.state.internalstate.InternalMarketState import exchange.dydx.abacus.state.internalstate.InternalState +import exchange.dydx.abacus.state.internalstate.InternalTradeInputState import exchange.dydx.abacus.state.manager.V4Environment import exchange.dydx.abacus.utils.GoodTil import exchange.dydx.abacus.validator.BaseInputValidator @@ -40,7 +45,31 @@ internal class TradeInputDataValidator( restricted: Boolean, environment: V4Environment? ): List? { - return null + val errors = mutableListOf() + + val marketId = internalState.input.trade.marketId ?: return null + val market = internalState.marketsSummary.markets[marketId] + + validateSize( + trade = internalState.input.trade, + market = market, + )?.let { + errors.addAll(it) + } + + validateLimitPrice( + trade = internalState.input.trade, + )?.let { + errors.addAll(it) + } + + validateTimeInForce( + trade = internalState.input.trade, + )?.let { + errors.addAll(it) + } + + return errors } override fun validateTradeDeprecated( @@ -60,7 +89,7 @@ internal class TradeInputDataValidator( trade: Map, ): List? { val errors = mutableListOf() - validateSize(trade, market)?.let { + validateSizeDeprecate(trade, market)?.let { /* ORDER_SIZE_BELOW_MIN_SIZE */ @@ -90,6 +119,42 @@ internal class TradeInputDataValidator( } private fun validateSize( + trade: InternalTradeInputState, + market: InternalMarketState?, + ): List? { + /* + ORDER_SIZE_BELOW_MIN_SIZE + */ + val symbol = market?.perpetualMarket?.assetId ?: return null + val size = trade.size?.size ?: return null + val minOrderSize = market.perpetualMarket?.configs?.minOrderSize ?: return null + return if (size.abs() < minOrderSize) { + listOf( + error( + type = ErrorType.error, + errorCode = "ORDER_SIZE_BELOW_MIN_SIZE", + fields = null, + actionStringKey = null, + titleStringKey = "ERRORS.TRADE_BOX_TITLE.ORDER_SIZE_BELOW_MIN_SIZE", + textStringKey = "ERRORS.TRADE_BOX.ORDER_SIZE_BELOW_MIN_SIZE", + textParams = mapOf( + "MIN_SIZE" to mapOf( + "value" to minOrderSize, + "format" to "size", + ), + "SYMBOL" to mapOf( + "value" to symbol, + "format" to "string", + ), + ), + ), + ) + } else { + null + } + } + + private fun validateSizeDeprecate( trade: Map, market: Map?, ): List? { @@ -130,6 +195,54 @@ internal class TradeInputDataValidator( return null } + private fun validateLimitPrice( + trade: InternalTradeInputState, + ): List? { + /* + LIMIT_MUST_ABOVE_TRIGGER_PRICE + LIMIT_MUST_BELOW_TRIGGER_PRICE + */ + return when (trade.type) { + OrderType.Limit, OrderType.TakeProfitLimit -> { + if (trade.execution != "IOC") { + return null + } + val side = trade.side ?: return null + val limitPrice = trade.price?.limitPrice ?: return null + val triggerPrice = trade.price?.triggerPrice ?: return null + if (side == OrderSide.Buy && limitPrice < triggerPrice) { + // BUY + return listOf( + error( + type = ErrorType.error, + errorCode = "LIMIT_MUST_ABOVE_TRIGGER_PRICE", + fields = listOf("price.triggerPrice"), + actionStringKey = "APP.TRADE.MODIFY_TRIGGER_PRICE", + titleStringKey = "ERRORS.TRADE_BOX_TITLE.LIMIT_MUST_ABOVE_TRIGGER_PRICE", + textStringKey = "ERRORS.TRADE_BOX.LIMIT_MUST_ABOVE_TRIGGER_PRICE", + ), + ) + } else if (side == OrderSide.Sell && limitPrice > triggerPrice) { + // SELL + return listOf( + error( + type = ErrorType.error, + errorCode = "LIMIT_MUST_BELOW_TRIGGER_PRICE", + fields = listOf("price.triggerPrice"), + actionStringKey = "APP.TRADE.MODIFY_TRIGGER_PRICE", + titleStringKey = "ERRORS.TRADE_BOX_TITLE.LIMIT_MUST_BELOW_TRIGGER_PRICE", + textStringKey = "ERRORS.TRADE_BOX.LIMIT_MUST_BELOW_TRIGGER_PRICE", + ), + ) + } else { + null + } + } + + else -> return null + } + } + private fun validateLimitPrice( trade: Map, market: Map?, @@ -185,6 +298,28 @@ internal class TradeInputDataValidator( } } + private fun validateTimeInForce( + trade: InternalTradeInputState, + ): List? { + if (trade.goodTil != null && trade.options.needsGoodUntil) { + val timeInterval = trade.goodTil?.timeInterval + if (timeInterval != null && timeInterval > 90.days) { + return listOf( + error( + type = ErrorType.error, + errorCode = "INVALID_GOOD_TIL", + fields = listOf("goodTil"), + actionStringKey = "APP.TRADE.MODIFY_GOOD_TIL", + titleStringKey = "ERRORS.TRADE_BOX_TITLE.INVALID_GOOD_TIL", + textStringKey = "ERRORS.TRADE_BOX.INVALID_GOOD_TIL_MAX_90_DAYS", + ), + ) + } + } + + return null + } + private fun validateTimeInForce( trade: Map, market: Map?, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeResctrictedValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeResctrictedValidator.kt index 02aaa9318..c85b2e212 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeResctrictedValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeResctrictedValidator.kt @@ -1,9 +1,11 @@ package exchange.dydx.abacus.validator.trade +import exchange.dydx.abacus.output.input.ErrorType import exchange.dydx.abacus.output.input.ValidationError import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.app.helper.Formatter +import exchange.dydx.abacus.state.internalstate.InternalMarketState import exchange.dydx.abacus.state.internalstate.InternalState import exchange.dydx.abacus.state.manager.V4Environment import exchange.dydx.abacus.validator.BaseInputValidator @@ -21,7 +23,16 @@ internal class TradeResctrictedValidator( restricted: Boolean, environment: V4Environment? ): List? { - return null + val marketId = internalState.input.trade.marketId ?: return null + val market = internalState.marketsSummary.markets[marketId] + val closeOnlyError = + validateClosingOnly( + market = market, + change = change, + restricted = restricted, + ) + + return closeOnlyError?.let { listOf(it) } } override fun validateTradeDeprecated( @@ -44,6 +55,71 @@ internal class TradeResctrictedValidator( return closeOnlyError?.let { listOf(it) } } + private fun validateClosingOnly( + market: InternalMarketState?, + change: PositionChange, + restricted: Boolean, + ): ValidationError? { + val marketId = market?.perpetualMarket?.assetId ?: "" + val canTrade = market?.perpetualMarket?.status?.canTrade ?: true + val canReduce = market?.perpetualMarket?.status?.canReduce ?: true + + return if (canTrade) { + if (restricted) { + when (change) { + PositionChange.NEW, PositionChange.INCREASING, PositionChange.CROSSING -> + error( + type = ErrorType.error, + errorCode = "RESTRICTED_USER", + fields = null, + actionStringKey = null, + titleStringKey = "ERRORS.TRADE_BOX_TITLE.MARKET_ORDER_CLOSE_POSITION_ONLY", + textStringKey = "ERRORS.TRADE_BOX.MARKET_ORDER_CLOSE_POSITION_ONLY", + ) + + else -> null + } + } else { + return null + } + } else if (canReduce) { + when (change) { + PositionChange.NEW, PositionChange.INCREASING, PositionChange.CROSSING -> + error( + type = ErrorType.error, + errorCode = "CLOSE_ONLY_MARKET", + fields = listOf("size.size"), + actionStringKey = "APP.TRADE.MODIFY_SIZE_FIELD", + titleStringKey = "WARNINGS.TRADE_BOX_TITLE.MARKET_STATUS_CLOSE_ONLY", + textStringKey = "WARNINGS.TRADE_BOX.MARKET_STATUS_CLOSE_ONLY", + textParams = mapOf( + "MARKET" to mapOf( + "value" to marketId, + "format" to "string", + ), + ), + ) + + else -> null + } + } else { + error( + type = ErrorType.error, + errorCode = "CLOSED_MARKET", + fields = null, + actionStringKey = null, + titleStringKey = "WARNINGS.TRADE_BOX_TITLE.MARKET_STATUS_CLOSE_ONLY", + textStringKey = "WARNINGS.TRADE_BOX.MARKET_STATUS_CLOSE_ONLY", + textParams = mapOf( + "MARKET" to mapOf( + "value" to marketId, + "format" to "string", + ), + ), + ) + } + } + private fun validateClosingOnlyDeprecated( parser: ParserProtocol, market: Map?, @@ -52,7 +128,7 @@ internal class TradeResctrictedValidator( ): Map? { val marketId = parser.asNativeMap(market?.get("assetId")) ?: "" val canTrade = parser.asBool(parser.value(market, "status.canTrade")) ?: true - val canReduce = parser.asBool(parser.value(market, "status.canTrade")) ?: true + val canReduce = parser.asBool(parser.value(market, "status.canReduce")) ?: true return if (canTrade) { if (restricted) { when (change) { From b847b400c9525999e8bfcd54f27003ec8093c193 Mon Sep 17 00:00:00 2001 From: Rui Date: Wed, 21 Aug 2024 05:05:06 +0900 Subject: [PATCH 48/63] TradeInputValidator --- .../output/input/TradeInput.kt | 4 +- .../input/TradeInputField+Actions.kt | 52 +-- .../state/internalstate/InternalState.kt | 2 +- .../state/model/TradingStateMachine.kt | 18 +- .../validator/AccountInputValidator.kt | 2 +- .../validator/BaseInputValidator.kt | 2 +- .../validator/FieldsInputValidator.kt | 2 +- .../validator/TradeInputValidator.kt | 3 +- .../validator/TransferInputValidator.kt | 2 +- .../validator/TriggerOrdersInputValidator.kt | 2 +- .../validator/ValidatorProtocols.kt | 1 + .../trade/TradeAccountStateValidator.kt | 255 +++++++++- .../trade/TradeBracketOrdersValidator.kt | 437 +++++++++++++++++- .../trade/TradeInputDataValidator.kt | 11 +- .../trade/TradeMarketOrderInputValidator.kt | 128 ++++- .../trade/TradePositionStateValidator.kt | 111 +++-- .../trade/TradeResctrictedValidator.kt | 1 + .../trade/TradeTriggerPriceValidator.kt | 255 +++++++++- .../transfer/WithdrawalCapacityValidator.kt | 2 +- 19 files changed, 1158 insertions(+), 132 deletions(-) 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 2cd8ad949..ff74c9291 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/output/input/TradeInput.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/TradeInput.kt @@ -753,7 +753,7 @@ enum class OrderType(val rawValue: String) { companion object { operator fun invoke(rawValue: String?) = - OrderType.values().firstOrNull { it.rawValue == rawValue } + entries.firstOrNull { it.rawValue == rawValue } } } @@ -855,7 +855,7 @@ data class TradeInput( fee = state.fee, marginMode = state.marginMode ?: MarginMode.Cross, targetLeverage = state.targetLeverage ?: 1.0, - bracket = state.bracket, + bracket = state.brackets, marketOrder = state.marketOrder, options = TradeInputOptions.create(state.options), summary = TradeInputSummary.create(state.summary), diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputField+Actions.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputField+Actions.kt index 302c12e7e..a6ce5981d 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputField+Actions.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputField+Actions.kt @@ -71,15 +71,15 @@ internal val TradeInputField.valueAction: ((InternalTradeInputState) -> Any?)? TradeInputField.reduceOnly -> { state -> state.reduceOnly } TradeInputField.postOnly -> { state -> state.postOnly } - TradeInputField.bracketsStopLossPrice -> { state -> state.bracket?.stopLoss?.triggerPrice } - TradeInputField.bracketsStopLossPercent -> { state -> state.bracket?.stopLoss?.percent } - TradeInputField.bracketsStopLossReduceOnly -> { state -> state.bracket?.stopLoss?.reduceOnly } - TradeInputField.bracketsTakeProfitPrice -> { state -> state.bracket?.takeProfit?.triggerPrice } - TradeInputField.bracketsTakeProfitPercent -> { state -> state.bracket?.takeProfit?.percent } - TradeInputField.bracketsTakeProfitReduceOnly -> { state -> state.bracket?.takeProfit?.reduceOnly } - TradeInputField.bracketsGoodUntilDuration -> { state -> state.bracket?.goodTil?.duration } - TradeInputField.bracketsGoodUntilUnit -> { state -> state.bracket?.goodTil?.unit } - TradeInputField.bracketsExecution -> { state -> state.bracket?.execution } + TradeInputField.bracketsStopLossPrice -> { state -> state.brackets?.stopLoss?.triggerPrice } + TradeInputField.bracketsStopLossPercent -> { state -> state.brackets?.stopLoss?.percent } + TradeInputField.bracketsStopLossReduceOnly -> { state -> state.brackets?.stopLoss?.reduceOnly } + TradeInputField.bracketsTakeProfitPrice -> { state -> state.brackets?.takeProfit?.triggerPrice } + TradeInputField.bracketsTakeProfitPercent -> { state -> state.brackets?.takeProfit?.percent } + TradeInputField.bracketsTakeProfitReduceOnly -> { state -> state.brackets?.takeProfit?.reduceOnly } + TradeInputField.bracketsGoodUntilDuration -> { state -> state.brackets?.goodTil?.duration } + TradeInputField.bracketsGoodUntilUnit -> { state -> state.brackets?.goodTil?.unit } + TradeInputField.bracketsExecution -> { state -> state.brackets?.execution } } // Returns the write action to update value for the trade input field @@ -110,29 +110,29 @@ internal val TradeInputField.updateValueAction: ((InternalTradeInputState, Strin } TradeInputField.bracketsStopLossPrice -> { trade, value, parser -> - val braket = TradeInputBracket.safeCreate(trade.bracket) + val braket = TradeInputBracket.safeCreate(trade.brackets) val stopLoss = TradeInputBracketSide.safeCreate(braket.stopLoss) - trade.bracket = + trade.brackets = braket.copy(stopLoss = stopLoss.copy(triggerPrice = parser.asDouble(value))) } TradeInputField.bracketsStopLossPercent -> { trade, value, parser -> - val braket = TradeInputBracket.safeCreate(trade.bracket) + val braket = TradeInputBracket.safeCreate(trade.brackets) val stopLoss = TradeInputBracketSide.safeCreate(braket.stopLoss) - trade.bracket = braket.copy(stopLoss = stopLoss.copy(percent = parser.asDouble(value))) + trade.brackets = braket.copy(stopLoss = stopLoss.copy(percent = parser.asDouble(value))) } TradeInputField.bracketsTakeProfitPrice -> { trade, value, parser -> - val braket = TradeInputBracket.safeCreate(trade.bracket) + val braket = TradeInputBracket.safeCreate(trade.brackets) val takeProfit = TradeInputBracketSide.safeCreate(braket.takeProfit) - trade.bracket = + trade.brackets = braket.copy(takeProfit = takeProfit.copy(triggerPrice = parser.asDouble(value))) } TradeInputField.bracketsTakeProfitPercent -> { trade, value, parser -> - val braket = TradeInputBracket.safeCreate(trade.bracket) + val braket = TradeInputBracket.safeCreate(trade.brackets) val takeProfit = TradeInputBracketSide.safeCreate(braket.takeProfit) - trade.bracket = + trade.brackets = braket.copy(takeProfit = takeProfit.copy(percent = parser.asDouble(value))) } @@ -149,8 +149,8 @@ internal val TradeInputField.updateValueAction: ((InternalTradeInputState, Strin } TradeInputField.bracketsGoodUntilUnit -> { trade, value, parser -> - val braket = TradeInputBracket.safeCreate(trade.bracket) - trade.bracket = braket.copy( + val braket = TradeInputBracket.safeCreate(trade.brackets) + trade.brackets = braket.copy( goodTil = TradeInputGoodUntil.safeCreate(braket.goodTil).copy(unit = value), ) } @@ -160,7 +160,7 @@ internal val TradeInputField.updateValueAction: ((InternalTradeInputState, Strin } TradeInputField.bracketsExecution -> { trade, value, parser -> - trade.bracket = TradeInputBracket.safeCreate(trade.bracket).copy(execution = value) + trade.brackets = TradeInputBracket.safeCreate(trade.brackets).copy(execution = value) } TradeInputField.goodTilDuration -> { trade, value, parser -> @@ -169,8 +169,8 @@ internal val TradeInputField.updateValueAction: ((InternalTradeInputState, Strin } TradeInputField.bracketsGoodUntilDuration -> { trade, value, parser -> - val braket = TradeInputBracket.safeCreate(trade.bracket) - trade.bracket = braket.copy( + val braket = TradeInputBracket.safeCreate(trade.brackets) + trade.brackets = braket.copy( goodTil = TradeInputGoodUntil.safeCreate(braket.goodTil) .copy(duration = parser.asDouble(value)), ) @@ -185,16 +185,16 @@ internal val TradeInputField.updateValueAction: ((InternalTradeInputState, Strin } TradeInputField.bracketsStopLossReduceOnly -> { trade, value, parser -> - val braket = TradeInputBracket.safeCreate(trade.bracket) + val braket = TradeInputBracket.safeCreate(trade.brackets) val stopLoss = TradeInputBracketSide.safeCreate(braket.stopLoss) - trade.bracket = + trade.brackets = braket.copy(stopLoss = stopLoss.copy(reduceOnly = parser.asBool(value) ?: false)) } TradeInputField.bracketsTakeProfitReduceOnly -> { trade, value, parser -> - val braket = TradeInputBracket.safeCreate(trade.bracket) + val braket = TradeInputBracket.safeCreate(trade.brackets) val takeProfit = TradeInputBracketSide.safeCreate(braket.takeProfit) - trade.bracket = braket.copy( + trade.brackets = braket.copy( takeProfit = takeProfit.copy( reduceOnly = parser.asBool(value) ?: false, ), diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt index 93bb8cc28..72449648c 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt @@ -85,7 +85,7 @@ internal data class InternalTradeInputState( var reduceOnly: Boolean = false, var postOnly: Boolean = false, var fee: Double? = null, - var bracket: TradeInputBracket? = null, + var brackets: TradeInputBracket? = null, var options: InternalTradeInputOptions = InternalTradeInputOptions(), var marketOrder: TradeInputMarketOrder? = null, var summary: InternalTradeInputSummary? = null, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt index 696069c96..d296ca4c5 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt @@ -1547,16 +1547,14 @@ open class TradingStateMachine( ) } - this.input?.let { - input = Input.create( - existing = input, - parser = parser, - data = it, - environment = environment, - internalState = internalState, - staticTyping = staticTyping, - ) - } + input = Input.create( + existing = input, + parser = parser, + data = this.input, + environment = environment, + internalState = internalState, + staticTyping = staticTyping, + ) } } if (changes.changes.contains(Changes.transferStatuses)) { diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/AccountInputValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/AccountInputValidator.kt index cb0360c68..25251765d 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/AccountInputValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/AccountInputValidator.kt @@ -22,7 +22,7 @@ internal class AccountInputValidator( inputType: InputType, environment: V4Environment?, ): List? { - TODO("Not yet implemented") + return null } override fun validateDeprecated( diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/BaseInputValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/BaseInputValidator.kt index 3befa132c..282b8d342 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/BaseInputValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/BaseInputValidator.kt @@ -91,7 +91,7 @@ internal open class BaseInputValidator( text = ErrorString( stringKey = textStringKey, params = params(parser, textParams)?.toIList(), - localized = localize(titleStringKey, textParams), + localized = localize(textStringKey, textParams), ), action = if (actionStringKey != null) { ErrorString( diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/FieldsInputValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/FieldsInputValidator.kt index adaca975a..396f7a9a5 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/FieldsInputValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/FieldsInputValidator.kt @@ -21,7 +21,7 @@ internal class FieldsInputValidator( inputType: InputType, environment: V4Environment?, ): List? { - TODO("Not yet implemented") + return null } override fun validateDeprecated( diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/TradeInputValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/TradeInputValidator.kt index 64b67daca..22137cd57 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/TradeInputValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/TradeInputValidator.kt @@ -43,7 +43,7 @@ internal class TradeInputValidator( environment: V4Environment?, ): List? { val transactionType = internalState.input.currentType - if (transactionType != InputType.TRANSFER || transactionType != InputType.CLOSE_POSITION) { + if (transactionType != InputType.TRADE && transactionType != InputType.CLOSE_POSITION) { return null } val change = getPositionChange( @@ -57,6 +57,7 @@ internal class TradeInputValidator( val validatorErrors = validator.validateTrade( internalState = internalState, + subaccountNumber = subaccountNumber ?: 0, change = change, restricted = restricted, environment = environment, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/TransferInputValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/TransferInputValidator.kt index 00ef3310d..0cfd1db3b 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/TransferInputValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/TransferInputValidator.kt @@ -32,7 +32,7 @@ internal class TransferInputValidator( inputType: InputType, environment: V4Environment?, ): List? { - TODO("Not yet implemented") + return null } override fun validateDeprecated( diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/TriggerOrdersInputValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/TriggerOrdersInputValidator.kt index 162663eb8..38bd13989 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/TriggerOrdersInputValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/TriggerOrdersInputValidator.kt @@ -35,7 +35,7 @@ internal class TriggerOrdersInputValidator( inputType: InputType, environment: V4Environment?, ): List? { - TODO("Not yet implemented") + return null } override fun validateDeprecated( diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/ValidatorProtocols.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/ValidatorProtocols.kt index 67809bf6a..780cdeb77 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/ValidatorProtocols.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/ValidatorProtocols.kt @@ -49,6 +49,7 @@ internal interface ValidatorProtocol { internal interface TradeValidatorProtocol { fun validateTrade( internalState: InternalState, + subaccountNumber: Int?, change: PositionChange, restricted: Boolean, environment: V4Environment?, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeAccountStateValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeAccountStateValidator.kt index e4feb24fc..9b69c56cd 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeAccountStateValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeAccountStateValidator.kt @@ -1,10 +1,19 @@ package exchange.dydx.abacus.validator.trade +import exchange.dydx.abacus.calculator.CalculationPeriod +import exchange.dydx.abacus.output.account.SubaccountOrder +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.ValidationError import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.app.helper.Formatter +import exchange.dydx.abacus.state.internalstate.InternalPerpetualPosition import exchange.dydx.abacus.state.internalstate.InternalState +import exchange.dydx.abacus.state.internalstate.InternalSubaccountState +import exchange.dydx.abacus.state.internalstate.InternalTradeInputState import exchange.dydx.abacus.state.manager.V4Environment import exchange.dydx.abacus.utils.NUM_PARENT_SUBACCOUNTS import exchange.dydx.abacus.utils.Numeric @@ -19,11 +28,60 @@ internal class TradeAccountStateValidator( ) : BaseInputValidator(localizer, formatter, parser), TradeValidatorProtocol { override fun validateTrade( internalState: InternalState, + subaccountNumber: Int?, change: PositionChange, restricted: Boolean, environment: V4Environment? ): List? { - return null + val trade = internalState.input.trade + val subaccountNumber = subaccountNumber ?: return null + val subaccount = internalState.wallet.account.subaccounts[subaccountNumber] ?: return null + val isIsolatedMarginTrade = subaccountNumber >= NUM_PARENT_SUBACCOUNTS + + val errors = mutableListOf() + when (isIsolatedMarginTrade) { + true -> { + validateSubaccountCrossOrders( + subaccount = subaccount, + trade = trade, + )?.let { + errors.add(it) + } + + validateSubaccountPostOrders( + subaccount = subaccount, + trade = trade, + change = change, + )?.let { + errors.add(it) + } + } + false -> { + validateSubaccountMarginUsage( + subaccount = subaccount, + change = change, + )?.let { + errors.add(it) + } + + validateSubaccountCrossOrders( + subaccount = subaccount, + trade = trade, + )?.let { + errors.add(it) + } + + validateSubaccountPostOrders( + subaccount = subaccount, + trade = trade, + change = change, + )?.let { + errors.add(it) + } + } + } + + return errors } override fun validateTradeDeprecated( @@ -43,7 +101,7 @@ internal class TradeAccountStateValidator( when (isIsolatedMarginTrade) { true -> { - val crossOrdersError = validateSubaccountCrossOrders( + val crossOrdersError = validateSubaccountCrossOrdersDeprecated( parser, subaccount, trade, @@ -51,7 +109,7 @@ internal class TradeAccountStateValidator( if (crossOrdersError != null) { errors.add(crossOrdersError) } - val postAllOrdersError = validateSubaccountPostOrders( + val postAllOrdersError = validateSubaccountPostOrdersDeprecated( parser, subaccount, trade, @@ -62,7 +120,7 @@ internal class TradeAccountStateValidator( } } false -> { - val marginError = validateSubaccountMarginUsage( + val marginError = validateSubaccountMarginUsageDeprecated( parser, subaccount, change, @@ -70,7 +128,7 @@ internal class TradeAccountStateValidator( if (marginError != null) { errors.add(marginError) } - val crossOrdersError = validateSubaccountCrossOrders( + val crossOrdersError = validateSubaccountCrossOrdersDeprecated( parser, subaccount, trade, @@ -78,7 +136,7 @@ internal class TradeAccountStateValidator( if (crossOrdersError != null) { errors.add(crossOrdersError) } - val postAllOrdersError = validateSubaccountPostOrders( + val postAllOrdersError = validateSubaccountPostOrdersDeprecated( parser, subaccount, trade, @@ -97,6 +155,41 @@ internal class TradeAccountStateValidator( } private fun validateSubaccountMarginUsage( + subaccount: InternalSubaccountState, + change: PositionChange, + ): ValidationError? { + /* + INVALID_NEW_ACCOUNT_MARGIN_USAGE + */ + return when (change) { + PositionChange.CLOSING, PositionChange.DECREASING -> null + else -> { + val equity = subaccount.calculated[CalculationPeriod.post]?.equity + val marginUsage = subaccount.calculated[CalculationPeriod.post]?.marginUsage + if (equity != null && + ( + equity == Numeric.double.ZERO || + marginUsage == null || + marginUsage < Numeric.double.ZERO || + marginUsage > Numeric.double.ONE + ) + ) { + error( + type = ErrorType.error, + errorCode = "INVALID_NEW_ACCOUNT_MARGIN_USAGE", + fields = listOf("size.size"), + actionStringKey = "APP.TRADE.MODIFY_SIZE_FIELD", + titleStringKey = "ERRORS.TRADE_BOX_TITLE.INVALID_NEW_ACCOUNT_MARGIN_USAGE", + textStringKey = "ERRORS.TRADE_BOX.INVALID_NEW_ACCOUNT_MARGIN_USAGE", + ) + } else { + null + } + } + } + } + + private fun validateSubaccountMarginUsageDeprecated( parser: ParserProtocol, subaccount: Map, change: PositionChange, @@ -133,6 +226,31 @@ internal class TradeAccountStateValidator( } private fun validateSubaccountCrossOrders( + subaccount: InternalSubaccountState, + trade: InternalTradeInputState, + ): ValidationError? { + /* + ORDER_CROSSES_OWN_ORDER + */ + return if (fillsExistingOrder( + trade = trade, + orders = subaccount.orders, + ) + ) { + error( + type = ErrorType.error, + errorCode = "ORDER_CROSSES_OWN_ORDER", + fields = listOf("size.size"), + actionStringKey = "APP.TRADE.MODIFY_SIZE_FIELD", + titleStringKey = "ERRORS.TRADE_BOX_TITLE.ORDER_CROSSES_OWN_ORDER", + textStringKey = "ERRORS.TRADE_BOX.ORDER_CROSSES_OWN_ORDER", + ) + } else { + null + } + } + + private fun validateSubaccountCrossOrdersDeprecated( parser: ParserProtocol, subaccount: Map, trade: Map, @@ -140,7 +258,7 @@ internal class TradeAccountStateValidator( /* ORDER_CROSSES_OWN_ORDER */ - return if (fillsExistingOrder( + return if (fillsExistingOrderDeprecated( parser, trade, parser.asNativeMap(subaccount["orders"]), @@ -160,6 +278,64 @@ internal class TradeAccountStateValidator( } private fun fillsExistingOrder( + trade: InternalTradeInputState, + orders: List?, + ): Boolean { + if (orders == null) { + return false + } + val type = trade.type ?: return false + val price = if (type == OrderType.Market) { + trade.marketOrder?.worstPrice + } else { + trade.summary?.price + } ?: return false + val marketId = trade.marketId ?: return false + val side = trade.side ?: return false + + val existing = orders.firstOrNull first@{ order -> + val orderPrice = order.price + val orderType = order.type + val orderMarketId = order.marketId + val orderStatus = order.status + val orderSide = order.side + if (orderMarketId == marketId && orderType == OrderType.Limit && orderStatus == OrderStatus.Open) { + when (side) { + OrderSide.Buy -> { + if (orderSide == OrderSide.Sell && price >= orderPrice) { + val stopPrice = trade.price?.triggerPrice + if (stopPrice != null) { + stopPrice < price + } else { + true + } + } else { + false + } + } + + OrderSide.Sell -> { + if (orderSide == OrderSide.Buy && price <= orderPrice) { + val stopPrice = trade.price?.triggerPrice + if (stopPrice != null) { + stopPrice > price + } else { + true + } + } else { + false + } + } + } + } else { + false + } + } + + return existing != null + } + + private fun fillsExistingOrderDeprecated( parser: ParserProtocol, trade: Map, orders: Map?, @@ -229,6 +405,44 @@ internal class TradeAccountStateValidator( } private fun validateSubaccountPostOrders( + subaccount: InternalSubaccountState, + trade: InternalTradeInputState, + change: PositionChange, + ): ValidationError? { + /* + ORDER_WITH_CURRENT_ORDERS_INVALID + */ + if (reducingWithLimit(change, trade.type)) { + return null + } + val positions = subaccount.openPositions + if (positions != null) { + var overleveraged = false + for ((_, value) in positions) { + val position = value + overleveraged = positionOverLeveragedPostAllOrders(position) + if (overleveraged) { + break + } + } + return if (overleveraged) { + error( + type = ErrorType.error, + errorCode = "ORDER_WITH_CURRENT_ORDERS_INVALID", + fields = null, + actionStringKey = null, + titleStringKey = "ERRORS.TRADE_BOX_TITLE.ORDER_WITH_CURRENT_ORDERS_INVALID", + textStringKey = "ERRORS.TRADE_BOX.ORDER_WITH_CURRENT_ORDERS_INVALID", + ) + } else { + null + } + } else { + return null + } + } + + private fun validateSubaccountPostOrdersDeprecated( parser: ParserProtocol, subaccount: Map, trade: Map, @@ -237,7 +451,7 @@ internal class TradeAccountStateValidator( /* ORDER_WITH_CURRENT_ORDERS_INVALID */ - return if (reducingWithLimit(parser, change, parser.asString(trade["type"]))) { + return if (reducingWithLimitDeprecated(parser, change, parser.asString(trade["type"]))) { null } else { val positions = parser.asNativeMap(subaccount["openPositions"]) @@ -245,7 +459,7 @@ internal class TradeAccountStateValidator( var overleveraged = false for ((_, value) in positions) { val position = parser.asNativeMap(value) - overleveraged = positionOverleveragedPostAllOrders(parser, position) + overleveraged = positionOverleveragedPostAllOrdersDeprecated(parser, position) if (overleveraged) { break } @@ -269,6 +483,16 @@ internal class TradeAccountStateValidator( } private fun reducingWithLimit( + change: PositionChange, + type: OrderType?, + ): Boolean { + return when (change) { + PositionChange.CLOSING, PositionChange.DECREASING -> true + else -> false + } && type == OrderType.Limit + } + + private fun reducingWithLimitDeprecated( parser: ParserProtocol, change: PositionChange, type: String?, @@ -279,7 +503,18 @@ internal class TradeAccountStateValidator( } && type == "LIMIT" } - private fun positionOverleveragedPostAllOrders( + private fun positionOverLeveragedPostAllOrders( + position: InternalPerpetualPosition?, + ): Boolean { + /* + ORDER_WITH_CURRENT_ORDERS_INVALID + */ + val leverage = position?.calculated?.get(CalculationPeriod.settled)?.leverage ?: return false + val adjustedImf = position?.calculated?.get(CalculationPeriod.settled)?.adjustedImf ?: return false + return if (adjustedImf > Numeric.double.ZERO) (leverage > Numeric.double.ONE / adjustedImf) else true + } + + private fun positionOverleveragedPostAllOrdersDeprecated( parser: ParserProtocol, position: Map?, ): Boolean { 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 b8a431de2..58524a09a 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeBracketOrdersValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeBracketOrdersValidator.kt @@ -1,10 +1,15 @@ package exchange.dydx.abacus.validator.trade +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.ValidationError import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.app.helper.Formatter +import exchange.dydx.abacus.state.internalstate.InternalPerpetualPosition import exchange.dydx.abacus.state.internalstate.InternalState +import exchange.dydx.abacus.state.internalstate.InternalTradeInputState import exchange.dydx.abacus.state.manager.V4Environment import exchange.dydx.abacus.utils.Numeric import exchange.dydx.abacus.validator.BaseInputValidator @@ -18,11 +23,28 @@ internal class TradeBracketOrdersValidator( ) : BaseInputValidator(localizer, formatter, parser), TradeValidatorProtocol { override fun validateTrade( internalState: InternalState, + subaccountNumber: Int?, change: PositionChange, restricted: Boolean, environment: V4Environment? ): List? { - return null + val trade = internalState.input.trade + if (!trade.options.needsBrackets) { + return null + } + + val marketId = trade.marketId ?: return null + val market = internalState.marketsSummary.markets[marketId] ?: return null + val subaccount = internalState.wallet.account.subaccounts[subaccountNumber] + val position = subaccount?.openPositions?.get(marketId) ?: return null + val price = trade.summary?.price ?: return null + val tickSize = market.perpetualMarket?.configs?.tickSize?.toString() ?: "0.01" + return validateBrackets( + position = position, + trade = trade, + price = price, + tickSize = tickSize, + ) } override fun validateTradeDeprecated( @@ -40,7 +62,7 @@ internal class TradeBracketOrdersValidator( parser.asNativeMap(parser.value(subaccount, "openPositions.$marketId")) ?: return null val price = parser.asDouble(parser.value(trade, "summary.price")) ?: return null val tickSize = parser.asString(parser.value(market, "configs.tickSize")) ?: "0.01" - return validateBrackets( + return validateBracketsDeprecated( position, trade, price, @@ -52,13 +74,33 @@ internal class TradeBracketOrdersValidator( } private fun validateBrackets( + position: InternalPerpetualPosition, + trade: InternalTradeInputState, + price: Double, + tickSize: String, + ): List { + val errors = mutableListOf() + + validateTakeProfit( + position = position, + trade = trade, + price = price, + tickSize = tickSize, + )?.let { + errors.add(it) + } + + return errors + } + + private fun validateBracketsDeprecated( position: Map, trade: Map, price: Double, tickSize: String, ): List? { val errors = mutableListOf() - val takeProfitError = validateTakeProfit( + val takeProfitError = validateTakeProfitDeprecated( position, trade, price, @@ -67,7 +109,7 @@ internal class TradeBracketOrdersValidator( if (takeProfitError != null) { errors.add(takeProfitError) } - val stopError = validateStopLoss( + val stopError = validateStopLossDeprecated( position, trade, price, @@ -80,6 +122,29 @@ internal class TradeBracketOrdersValidator( } private fun validateTakeProfit( + position: InternalPerpetualPosition, + trade: InternalTradeInputState, + price: Double, + tickSize: String, + ): ValidationError? { + val triggerPrice = trade.brackets?.takeProfit?.triggerPrice ?: return null + return validateTakeProfitTriggerToMarketPrice( + trade = trade, + triggerPrice = triggerPrice, + price = price, + tickSize = tickSize, + ) ?: validateTakeProfitTriggerToLiquidationPrice( + trade = trade, + position = position, + triggerPrice = triggerPrice, + tickSize = tickSize, + ) ?: validateTakeProfitReduceOnly( + trade = trade, + position = position, + ) + } + + private fun validateTakeProfitDeprecated( position: Map, trade: Map, price: Double, @@ -87,17 +152,66 @@ internal class TradeBracketOrdersValidator( ): Map? { val triggerPrice = parser.asDouble(parser.value(trade, "brackets.takeProfit.triggerPrice")) ?: return null - return validateTakeProfitTriggerToMarketPrice(trade, triggerPrice, price, tickSize) - ?: validateTakeProfitTriggerToLiquidationPrice( + return validateTakeProfitTriggerToMarketPriceDeprecated(trade, triggerPrice, price, tickSize) + ?: validateTakeProfitTriggerToLiquidationPriceDeprecated( trade, position, triggerPrice, tickSize, ) - ?: validateTakeProfitReduceOnly(trade, position) + ?: validateTakeProfitReduceOnlyDeprecated(trade, position) } private fun validateTakeProfitTriggerToMarketPrice( + trade: InternalTradeInputState, + triggerPrice: Double, + price: Double, + tickSize: String, + ): ValidationError? { + when (trade.side) { + OrderSide.Buy -> { + if (triggerPrice >= price) { + return triggerPriceError( + errorCode = "BRACKET_ORDER_TAKE_PROFIT_BELOW_EXPECTED_PRICE", + fields = listOf("brackets.takeProfit.triggerPrice", "brackets.takeProfit.percent"), + title = "ERRORS.TRADE_BOX_TITLE.BRACKET_ORDER_TAKE_PROFIT_BELOW_EXPECTED_PRICE", + text = "ERRORS.TRADE_BOX.BRACKET_ORDER_TAKE_PROFIT_BELOW_EXPECTED_PRICE", + params = mapOf( + "EXPECTED_PRICE" to mapOf( + "value" to price, + "format" to "price", + "tickSize" to tickSize, + ), + ), + ) + } else { + return null + } + } + OrderSide.Sell -> { + if (triggerPrice <= price) { + return triggerPriceError( + errorCode = "BRACKET_ORDER_TAKE_PROFIT_ABOVE_EXPECTED_PRICE", + fields = listOf("brackets.takeProfit.triggerPrice", "brackets.takeProfit.percent"), + title = "ERRORS.TRADE_BOX_TITLE.BRACKET_ORDER_TAKE_PROFIT_ABOVE_EXPECTED_PRICE", + text = "ERRORS.TRADE_BOX.BRACKET_ORDER_TAKE_PROFIT_ABOVE_EXPECTED_PRICE", + params = mapOf( + "EXPECTED_PRICE" to mapOf( + "value" to price, + "format" to "price", + "tickSize" to tickSize, + ), + ), + ) + } else { + return null + } + } + else -> return null + } + } + + private fun validateTakeProfitTriggerToMarketPriceDeprecated( trade: Map, triggerPrice: Double, price: Double, @@ -106,7 +220,7 @@ internal class TradeBracketOrdersValidator( return when (parser.asString(trade["side"])) { "SELL" -> { if (triggerPrice >= price) { - triggerPriceError( + triggerPriceErrorDeprecated( "BRACKET_ORDER_TAKE_PROFIT_BELOW_EXPECTED_PRICE", listOf("brackets.takeProfit.triggerPrice", "brackets.takeProfit.percent"), "ERRORS.TRADE_BOX_TITLE.BRACKET_ORDER_TAKE_PROFIT_BELOW_EXPECTED_PRICE", @@ -126,7 +240,7 @@ internal class TradeBracketOrdersValidator( "BUY" -> { if (triggerPrice <= price) { - triggerPriceError( + triggerPriceErrorDeprecated( "BRACKET_ORDER_TAKE_PROFIT_ABOVE_EXPECTED_PRICE", listOf("brackets.takeProfit.triggerPrice", "brackets.takeProfit.percent"), "ERRORS.TRADE_BOX_TITLE.BRACKET_ORDER_TAKE_PROFIT_ABOVE_EXPECTED_PRICE", @@ -149,6 +263,59 @@ internal class TradeBracketOrdersValidator( } private fun validateTakeProfitTriggerToLiquidationPrice( + trade: InternalTradeInputState, + position: InternalPerpetualPosition, + triggerPrice: Double, + tickSize: String, + ): ValidationError? { + val sizePostOrder = position.calculated[CalculationPeriod.post]?.size ?: Numeric.double.ZERO + val liquidationPrice = position.calculated[CalculationPeriod.post]?.liquidationPrice + ?: return null + + when (trade.side) { + OrderSide.Sell -> { + if (sizePostOrder < Numeric.double.ZERO && triggerPrice > liquidationPrice) { + return triggerPriceError( + errorCode = "BRACKET_ORDER_TAKE_PROFIT_BELOW_LIQUIDATION_PRICE", + fields = listOf("brackets.takeProfit.triggerPrice", "brackets.takeProfit.reduceOnly"), + title = "ERRORS.TRADE_BOX_TITLE.BRACKET_ORDER_TAKE_PROFIT_BELOW_LIQUIDATION_PRICE", + text = "ERRORS.TRADE_BOX.BRACKET_ORDER_TAKE_PROFIT_BELOW_LIQUIDATION_PRICE", + params = mapOf( + "TRIGGER_PRICE_LIMIT" to mapOf( + "value" to liquidationPrice, + "format" to "price", + "tickSize" to tickSize, + ), + ), + ) + } else { + return null + } + } + OrderSide.Buy -> { + if (sizePostOrder > Numeric.double.ZERO && triggerPrice < liquidationPrice) { + return triggerPriceError( + errorCode = "BRACKET_ORDER_TAKE_PROFIT_ABOVE_LIQUIDATION_PRICE", + fields = listOf("brackets.takeProfit.triggerPrice", "brackets.takeProfit.reduceOnly"), + title = "ERRORS.TRADE_BOX_TITLE.BRACKET_ORDER_TAKE_PROFIT_ABOVE_LIQUIDATION_PRICE", + text = "ERRORS.TRADE_BOX.BRACKET_ORDER_TAKE_PROFIT_ABOVE_LIQUIDATION_PRICE", + params = mapOf( + "TRIGGER_PRICE_LIMIT" to mapOf( + "value" to liquidationPrice, + "format" to "price", + "tickSize" to tickSize, + ), + ), + ) + } else { + return null + } + } + else -> return null + } + } + + private fun validateTakeProfitTriggerToLiquidationPriceDeprecated( trade: Map, position: Map, triggerPrice: Double, @@ -162,7 +329,7 @@ internal class TradeBracketOrdersValidator( return when (parser.asString(trade["side"])) { "SELL" -> { if (sizePostOrder > Numeric.double.ZERO && triggerPrice < liquidationPrice) { - triggerPriceError( + triggerPriceErrorDeprecated( "BRACKET_ORDER_TAKE_PROFIT_ABOVE_LIQUIDATION_PRICE", listOf( "brackets.takeProfit.triggerPrice", @@ -185,7 +352,7 @@ internal class TradeBracketOrdersValidator( "BUY" -> { if (sizePostOrder < Numeric.double.ZERO && triggerPrice > liquidationPrice) { - triggerPriceError( + triggerPriceErrorDeprecated( "BRACKET_ORDER_TAKE_PROFIT_BELOW_LIQUIDATION_PRICE", listOf( "brackets.takeProfit.triggerPrice", @@ -211,6 +378,39 @@ internal class TradeBracketOrdersValidator( } private fun validateTakeProfitReduceOnly( + trade: InternalTradeInputState, + position: InternalPerpetualPosition, + ): ValidationError? { + val reduceOnly = trade.brackets?.takeProfit?.reduceOnly ?: false + val sizePostOrder = position.calculated[CalculationPeriod.post]?.size ?: Numeric.double.ZERO + return if (reduceOnly) { + when (trade.side) { + OrderSide.Sell -> { + if (sizePostOrder > Numeric.double.ZERO) { + reduceOnlyError( + field = listOf("brackets.takeProfit.triggerPrice", "brackets.takeProfit.reduceOnly"), + ) + } else { + null + } + } + OrderSide.Buy -> { + if (sizePostOrder < Numeric.double.ZERO) { + reduceOnlyError( + field = listOf("brackets.takeProfit.triggerPrice", "brackets.takeProfit.reduceOnly"), + ) + } else { + null + } + } + else -> null + } + } else { + null + } + } + + private fun validateTakeProfitReduceOnlyDeprecated( trade: Map, position: Map, ): Map? { @@ -222,7 +422,7 @@ internal class TradeBracketOrdersValidator( when (parser.asString(trade["side"])) { "SELL" -> { if (sizePostOrder > 0.0) { - reduceOnlyError( + reduceOnlyErrorDeprecated( listOf("brackets.takeProfit.triggerPrice", "brackets.takeProfit.reduceOnly"), ) } else { @@ -232,7 +432,7 @@ internal class TradeBracketOrdersValidator( "BUY" -> { if (sizePostOrder < 0.0) { - reduceOnlyError( + reduceOnlyErrorDeprecated( listOf("brackets.takeProfit.triggerPrice", "brackets.takeProfit.reduceOnly"), ) } else { @@ -248,6 +448,29 @@ internal class TradeBracketOrdersValidator( } private fun validateStopLoss( + position: InternalPerpetualPosition, + trade: InternalTradeInputState, + price: Double, + tickSize: String, + ): ValidationError? { + val triggerPrice = trade.brackets?.stopLoss?.triggerPrice ?: return null + return validateStopLossTriggerToMarketPrice( + trade = trade, + triggerPrice = triggerPrice, + price = price, + tickSize = tickSize, + ) ?: validateStopLossTriggerToLiquidationPrice( + trade = trade, + position = position, + triggerPrice = triggerPrice, + tickSize = tickSize, + ) ?: validateStopLossReduceOnly( + trade = trade, + position = position, + ) + } + + private fun validateStopLossDeprecated( position: Map, trade: Map, price: Double, @@ -255,17 +478,66 @@ internal class TradeBracketOrdersValidator( ): Map? { val triggerPrice = parser.asDouble(parser.value(trade, "brackets.stopLoss.triggerPrice")) ?: return null - return validateStopLossTriggerToMarketPrice(trade, triggerPrice, price, tickSize) - ?: validateStopLossTriggerToLiquidationPrice( + return validateStopLossTriggerToMarketPriceDeprecated(trade, triggerPrice, price, tickSize) + ?: validateStopLossTriggerToLiquidationPriceDeprecated( trade, position, triggerPrice, tickSize, ) - ?: validateStopLossReduceOnly(trade, position) + ?: validateStopLossReduceOnlyDeprecated(trade, position) } private fun validateStopLossTriggerToMarketPrice( + trade: InternalTradeInputState, + triggerPrice: Double, + price: Double, + tickSize: String, + ): ValidationError? { + when (trade.side) { + OrderSide.Sell -> { + if (triggerPrice <= price) { + return triggerPriceError( + errorCode = "BRACKET_ORDER_STOP_LOSS_ABOVE_EXPECTED_PRICE", + fields = listOf("brackets.stopLoss.triggerPrice", "brackets.stopLoss.percent"), + title = "ERRORS.TRADE_BOX_TITLE.BRACKET_ORDER_STOP_LOSS_ABOVE_EXPECTED_PRICE", + text = "ERRORS.TRADE_BOX.BRACKET_ORDER_STOP_LOSS_ABOVE_EXPECTED_PRICE", + params = mapOf( + "EXPECTED_PRICE" to mapOf( + "value" to price, + "format" to "price", + "tickSize" to tickSize, + ), + ), + ) + } else { + return null + } + } + OrderSide.Buy -> { + if (triggerPrice >= price) { + return triggerPriceError( + errorCode = "BRACKET_ORDER_STOP_LOSS_BELOW_EXPECTED_PRICE", + fields = listOf("brackets.stopLoss.triggerPrice", "brackets.stopLoss.percent"), + title = "ERRORS.TRADE_BOX_TITLE.BRACKET_ORDER_STOP_LOSS_BELOW_EXPECTED_PRICE", + text = "ERRORS.TRADE_BOX.BRACKET_ORDER_STOP_LOSS_BELOW_EXPECTED_PRICE", + params = mapOf( + "EXPECTED_PRICE" to mapOf( + "value" to price, + "format" to "price", + "tickSize" to tickSize, + ), + ), + ) + } else { + return null + } + } + else -> return null + } + } + + private fun validateStopLossTriggerToMarketPriceDeprecated( trade: Map, triggerPrice: Double, price: Double, @@ -274,7 +546,7 @@ internal class TradeBracketOrdersValidator( return when (parser.asString(trade["side"])) { "SELL" -> { if (triggerPrice <= price) { - triggerPriceError( + triggerPriceErrorDeprecated( "BRACKET_ORDER_STOP_LOSS_ABOVE_EXPECTED_PRICE", listOf("brackets.stopLoss.triggerPrice", "brackets.stopLoss.percent"), "ERRORS.TRADE_BOX_TITLE.BRACKET_ORDER_STOP_LOSS_ABOVE_EXPECTED_PRICE", @@ -294,7 +566,7 @@ internal class TradeBracketOrdersValidator( "BUY" -> { if (triggerPrice >= price) { - triggerPriceError( + triggerPriceErrorDeprecated( "BRACKET_ORDER_STOP_LOSS_BELOW_EXPECTED_PRICE", listOf("brackets.stopLoss.triggerPrice", "brackets.stopLoss.percent"), "ERRORS.TRADE_BOX_TITLE.BRACKET_ORDER_STOP_LOSS_BELOW_EXPECTED_PRICE", @@ -317,6 +589,59 @@ internal class TradeBracketOrdersValidator( } private fun validateStopLossTriggerToLiquidationPrice( + trade: InternalTradeInputState, + position: InternalPerpetualPosition, + triggerPrice: Double, + tickSize: String, + ): ValidationError? { + val sizePostOrder = position.calculated[CalculationPeriod.post]?.size ?: Numeric.double.ZERO + val liquidationPrice = position.calculated[CalculationPeriod.post]?.liquidationPrice + ?: return null + + when (trade.side) { + OrderSide.Sell -> { + if (sizePostOrder < Numeric.double.ZERO && triggerPrice > liquidationPrice) { + return triggerPriceError( + errorCode = "BRACKET_ORDER_STOP_LOSS_BELOW_LIQUIDATION_PRICE", + fields = listOf("brackets.stopLoss.triggerPrice", "brackets.stopLoss.percent"), + title = "ERRORS.TRADE_BOX_TITLE.BRACKET_ORDER_STOP_LOSS_BELOW_LIQUIDATION_PRICE", + text = "ERRORS.TRADE_BOX.BRACKET_ORDER_STOP_LOSS_BELOW_LIQUIDATION_PRICE", + params = mapOf( + "TRIGGER_PRICE_LIMIT" to mapOf( + "value" to liquidationPrice, + "format" to "price", + "tickSize" to tickSize, + ), + ), + ) + } else { + return null + } + } + OrderSide.Buy -> { + if (sizePostOrder > Numeric.double.ZERO && triggerPrice < liquidationPrice) { + return triggerPriceError( + errorCode = "BRACKET_ORDER_STOP_LOSS_ABOVE_LIQUIDATION_PRICE", + fields = listOf("brackets.stopLoss.triggerPrice", "brackets.stopLoss.percent"), + title = "ERRORS.TRADE_BOX_TITLE.BRACKET_ORDER_STOP_LOSS_ABOVE_LIQUIDATION_PRICE", + text = "ERRORS.TRADE_BOX.BRACKET_ORDER_STOP_LOSS_ABOVE_LIQUIDATION_PRICE", + params = mapOf( + "TRIGGER_PRICE_LIMIT" to mapOf( + "value" to liquidationPrice, + "format" to "price", + "tickSize" to tickSize, + ), + ), + ) + } else { + return null + } + } + else -> return null + } + } + + private fun validateStopLossTriggerToLiquidationPriceDeprecated( trade: Map, position: Map, triggerPrice: Double, @@ -330,7 +655,7 @@ internal class TradeBracketOrdersValidator( return when (parser.asString(trade["side"])) { "SELL" -> { if (sizePostOrder < Numeric.double.ZERO && triggerPrice > liquidationPrice) { - triggerPriceError( + triggerPriceErrorDeprecated( "BRACKET_ORDER_STOP_LOSS_BELOW_LIQUIDATION_PRICE", listOf("brackets.stopLoss.triggerPrice", "brackets.stopLoss.percent"), "ERRORS.TRADE_BOX_TITLE.BRACKET_ORDER_STOP_LOSS_BELOW_LIQUIDATION_PRICE", @@ -350,7 +675,7 @@ internal class TradeBracketOrdersValidator( "BUY" -> { if (sizePostOrder > Numeric.double.ZERO && triggerPrice < liquidationPrice) { - triggerPriceError( + triggerPriceErrorDeprecated( "BRACKET_ORDER_STOP_LOSS_ABOVE_LIQUIDATION_PRICE", listOf("brackets.stopLoss.triggerPrice", "brackets.stopLoss.percent"), "ERRORS.TRADE_BOX_TITLE.BRACKET_ORDER_STOP_LOSS_ABOVE_LIQUIDATION_PRICE", @@ -373,6 +698,39 @@ internal class TradeBracketOrdersValidator( } private fun validateStopLossReduceOnly( + trade: InternalTradeInputState, + position: InternalPerpetualPosition, + ): ValidationError? { + val reduceOnly = trade.brackets?.stopLoss?.reduceOnly ?: false + val sizePostOrder = position.calculated[CalculationPeriod.post]?.size ?: Numeric.double.ZERO + return if (reduceOnly) { + when (trade.side) { + OrderSide.Sell -> { + if (sizePostOrder > Numeric.double.ZERO) { + reduceOnlyError( + field = listOf("brackets.stopLoss.triggerPrice", "brackets.stopLoss.reduceOnly"), + ) + } else { + null + } + } + OrderSide.Buy -> { + if (sizePostOrder < Numeric.double.ZERO) { + reduceOnlyError( + field = listOf("brackets.stopLoss.triggerPrice", "brackets.stopLoss.reduceOnly"), + ) + } else { + null + } + } + else -> null + } + } else { + null + } + } + + private fun validateStopLossReduceOnlyDeprecated( trade: Map, position: Map, ): Map? { @@ -383,7 +741,7 @@ internal class TradeBracketOrdersValidator( when (parser.asString(trade["side"])) { "SELL" -> { if (sizePostOrder > 0.0) { - reduceOnlyError( + reduceOnlyErrorDeprecated( listOf("brackets.stopLoss.triggerPrice", "brackets.stopLoss.reduceOnly"), ) } else { @@ -393,7 +751,7 @@ internal class TradeBracketOrdersValidator( "BUY" -> { if (sizePostOrder < 0.0) { - reduceOnlyError( + reduceOnlyErrorDeprecated( listOf("brackets.stopLoss.triggerPrice", "brackets.stopLoss.reduceOnly"), ) } else { @@ -408,7 +766,7 @@ internal class TradeBracketOrdersValidator( } } - private fun triggerPriceError( + private fun triggerPriceErrorDeprecated( errorCode: String, fields: List, title: String, @@ -426,7 +784,25 @@ internal class TradeBracketOrdersValidator( ) } - private fun reduceOnlyError( + private fun triggerPriceError( + errorCode: String, + fields: List, + title: String, + text: String, + params: Map?, + ): ValidationError { + return error( + type = ErrorType.error, + errorCode = errorCode, + fields = fields, + actionStringKey = "APP.TRADE.ENTER_TRIGGER_PRICE", + titleStringKey = title, + textStringKey = text, + textParams = params, + ) + } + + private fun reduceOnlyErrorDeprecated( field: List, ): Map { return errorDeprecated( @@ -438,4 +814,17 @@ internal class TradeBracketOrdersValidator( textStringKey = "ERRORS.TRADE_BOX.WOULD_NOT_REDUCE_UNCHECK", ) } + + private fun reduceOnlyError( + field: List, + ): ValidationError { + return error( + type = ErrorType.error, + errorCode = "WOULD_NOT_REDUCE_UNCHECK", + fields = field, + actionStringKey = "APP.TRADE.ENTER_TRIGGER_PRICE", + titleStringKey = "ERRORS.TRADE_BOX_TITLE.WOULD_NOT_REDUCE_UNCHECK", + textStringKey = "ERRORS.TRADE_BOX.WOULD_NOT_REDUCE_UNCHECK", + ) + } } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeInputDataValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeInputDataValidator.kt index cbfc90281..68db12cf3 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeInputDataValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeInputDataValidator.kt @@ -41,6 +41,7 @@ internal class TradeInputDataValidator( override fun validateTrade( internalState: InternalState, + subaccountNumber: Int?, change: PositionChange, restricted: Boolean, environment: V4Environment? @@ -95,7 +96,7 @@ internal class TradeInputDataValidator( */ errors.addAll(it) } - validateLimitPrice(trade, market)?.let { + validateLimitPriceDeprecated(trade, market)?.let { /* LIMIT_MUST_ABOVE_TRIGGER_PRICE LIMIT_MUST_BELOW_TRIGGER_PRICE @@ -105,7 +106,7 @@ internal class TradeInputDataValidator( errors.addAll(it) } - validateTimeInForce(trade, market)?.let { + validateTimeInForceDeprecated(trade, market)?.let { /* LIMIT_MUST_ABOVE_TRIGGER_PRICE LIMIT_MUST_BELOW_TRIGGER_PRICE @@ -203,7 +204,7 @@ internal class TradeInputDataValidator( LIMIT_MUST_BELOW_TRIGGER_PRICE */ return when (trade.type) { - OrderType.Limit, OrderType.TakeProfitLimit -> { + OrderType.StopLimit, OrderType.TakeProfitLimit -> { if (trade.execution != "IOC") { return null } @@ -243,7 +244,7 @@ internal class TradeInputDataValidator( } } - private fun validateLimitPrice( + private fun validateLimitPriceDeprecated( trade: Map, market: Map?, ): List? { @@ -320,7 +321,7 @@ internal class TradeInputDataValidator( return null } - private fun validateTimeInForce( + private fun validateTimeInForceDeprecated( trade: Map, market: Map?, ): List? { diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeMarketOrderInputValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeMarketOrderInputValidator.kt index 8e4cb09bf..216409ebe 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeMarketOrderInputValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeMarketOrderInputValidator.kt @@ -1,11 +1,14 @@ package exchange.dydx.abacus.validator.trade import abs +import exchange.dydx.abacus.output.input.ErrorType +import exchange.dydx.abacus.output.input.OrderType import exchange.dydx.abacus.output.input.ValidationError import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.app.helper.Formatter import exchange.dydx.abacus.state.internalstate.InternalState +import exchange.dydx.abacus.state.internalstate.InternalTradeInputState import exchange.dydx.abacus.state.manager.V4Environment import exchange.dydx.abacus.validator.BaseInputValidator import exchange.dydx.abacus.validator.PositionChange @@ -21,11 +24,24 @@ internal class TradeMarketOrderInputValidator( override fun validateTrade( internalState: InternalState, + subaccountNumber: Int?, change: PositionChange, restricted: Boolean, environment: V4Environment? ): List? { - return null + val trade = internalState.input.trade + if (trade.type != OrderType.Market) { + return null + } + + val errors = mutableListOf() + validateLiquidity(trade)?.let { + errors.add(it) + } + validateOrderbookOrIndexSlippage(trade, restricted)?.let { + errors.add(it) + } + return errors } override fun validateTradeDeprecated( @@ -75,6 +91,38 @@ internal class TradeMarketOrderInputValidator( } } + private fun validateLiquidity( + trade: InternalTradeInputState, + ): ValidationError? { + /* + MARKET_ORDER_NOT_ENOUGH_LIQUIDITY + */ + val filled = trade.marketOrder?.filled + + if (filled == false) { + return createTradeBoxWarningOrError( + errorLevel = if (accountRestricted()) ErrorType.warning else ErrorType.error, + errorCode = "MARKET_ORDER_NOT_ENOUGH_LIQUIDITY", + fields = listOf("size.size"), + actionStringKey = "APP.TRADE.MODIFY_SIZE_FIELD", + ) + } + + val summary = trade.summary + // if there's liquidity for market order to be filled but is missing orderbook slippage (mid price) + // it is a one sided liquidity situation and should place limit order instead + if (summary != null && summary.slippage == null) { + return createTradeBoxWarningOrError( + errorLevel = if (accountRestricted()) ErrorType.warning else ErrorType.error, + errorCode = "MARKET_ORDER_ONE_SIDED_LIQUIDITY", + fields = listOf("size.size"), + actionStringKey = "APP.TRADE.MODIFY_SIZE_FIELD", + ) + } + + return null + } + private fun liquidity( trade: Map, restricted: Boolean @@ -85,7 +133,7 @@ internal class TradeMarketOrderInputValidator( val filled = parser.asBool(parser.value(trade, "marketOrder.filled")) if (filled == false) { - return createTradeBoxWarningOrError( + return createTradeBoxWarningOrErrorDeprecated( errorLevel = if (restricted) "WARNING" else "ERROR", errorCode = "MARKET_ORDER_NOT_ENOUGH_LIQUIDITY", fields = listOf("size.size"), @@ -97,7 +145,7 @@ internal class TradeMarketOrderInputValidator( // if there's liquidity for market order to be filled but is missing orderbook slippage (mid price) // it is a one sided liquidity situation and should place limit order instead parser.asDouble(summary["slippage"]) - ?: return createTradeBoxWarningOrError( + ?: return createTradeBoxWarningOrErrorDeprecated( errorLevel = if (restricted) "WARNING" else "ERROR", errorCode = "MARKET_ORDER_ONE_SIDED_LIQUIDITY", fields = listOf("size.size"), @@ -107,6 +155,49 @@ internal class TradeMarketOrderInputValidator( return null } + private fun validateOrderbookOrIndexSlippage( + trade: InternalTradeInputState, + restricted: Boolean, + ): ValidationError? { + /* + MARKET_ORDER_WARNING_ORDERBOOK_SLIPPAGE + MARKET_ORDER_ERROR_ORDERBOOK_SLIPPAGE + + MARKET_ORDER_WARNING_INDEX_PRICE_SLIPPAGE + MARKET_ORDER_ERROR_INDEX_PRICE_SLIPPAGE + */ + val summary = trade.summary ?: return null + + // missing orderbook slippage is due to a one sided liquidity situation + // and should be caught by liquidity validation + val orderbookSlippage = summary.slippage ?: return null + val orderbookSlippageValue = orderbookSlippage.abs() + val indexSlippage = summary.indexSlippage + + var slippageType = "ORDERBOOK" + var minSlippageValue = orderbookSlippageValue + if (indexSlippage != null && indexSlippage < orderbookSlippageValue) { + slippageType = "INDEX_PRICE" + minSlippageValue = indexSlippage + } + + return when { + minSlippageValue >= marketOrderErrorSlippage -> createTradeBoxWarningOrError( + errorLevel = if (restricted) ErrorType.warning else ErrorType.error, + errorCode = "MARKET_ORDER_ERROR_${slippageType}_SLIPPAGE", + actionStringKey = "APP.TRADE.PLACE_LIMIT_ORDER", + slippagePercentValue = minSlippageValue, + ) + minSlippageValue >= marketOrderWarningSlippage -> createTradeBoxWarningOrError( + errorLevel = ErrorType.warning, + errorCode = "MARKET_ORDER_WARNING_${slippageType}_SLIPPAGE", + actionStringKey = "APP.TRADE.PLACE_LIMIT_ORDER", + slippagePercentValue = minSlippageValue, + ) + else -> null + } + } + private fun orderbookOrIndexSlippage( trade: Map, restricted: Boolean @@ -134,13 +225,13 @@ internal class TradeMarketOrderInputValidator( } return when { - minSlippageValue >= marketOrderErrorSlippage -> createTradeBoxWarningOrError( + minSlippageValue >= marketOrderErrorSlippage -> createTradeBoxWarningOrErrorDeprecated( errorLevel = if (restricted) "WARNING" else "ERROR", errorCode = "MARKET_ORDER_ERROR_${slippageType}_SLIPPAGE", actionStringKey = "APP.TRADE.PLACE_LIMIT_ORDER", slippagePercentValue = minSlippageValue, ) - minSlippageValue >= marketOrderWarningSlippage -> createTradeBoxWarningOrError( + minSlippageValue >= marketOrderWarningSlippage -> createTradeBoxWarningOrErrorDeprecated( errorLevel = "WARNING", errorCode = "MARKET_ORDER_WARNING_${slippageType}_SLIPPAGE", actionStringKey = "APP.TRADE.PLACE_LIMIT_ORDER", @@ -150,7 +241,7 @@ internal class TradeMarketOrderInputValidator( } } - private fun createTradeBoxWarningOrError( + private fun createTradeBoxWarningOrErrorDeprecated( errorLevel: String, errorCode: String, fields: List? = null, @@ -174,4 +265,29 @@ internal class TradeMarketOrderInputValidator( }, ) } + + private fun createTradeBoxWarningOrError( + errorLevel: ErrorType, + errorCode: String, + fields: List? = null, + actionStringKey: String? = null, + slippagePercentValue: Double? = null + ): ValidationError { + return error( + type = errorLevel, + errorCode = errorCode, + fields = fields, + actionStringKey = actionStringKey, + titleStringKey = "ERRORS.TRADE_BOX_TITLE.$errorCode", + textStringKey = "ERRORS.TRADE_BOX.$errorCode", + textParams = slippagePercentValue?.let { + mapOf( + "SLIPPAGE" to mapOf( + "value" to it, + "format" to "percent", + ), + ) + }, + ) + } } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradePositionStateValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradePositionStateValidator.kt index 51523cc36..98152042d 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradePositionStateValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradePositionStateValidator.kt @@ -1,10 +1,15 @@ package exchange.dydx.abacus.validator.trade +import exchange.dydx.abacus.calculator.CalculationPeriod +import exchange.dydx.abacus.output.input.ErrorType import exchange.dydx.abacus.output.input.ValidationError import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.app.helper.Formatter +import exchange.dydx.abacus.state.internalstate.InternalMarketState +import exchange.dydx.abacus.state.internalstate.InternalPerpetualPosition import exchange.dydx.abacus.state.internalstate.InternalState +import exchange.dydx.abacus.state.internalstate.InternalTradeInputState import exchange.dydx.abacus.state.manager.V4Environment import exchange.dydx.abacus.utils.Numeric import exchange.dydx.abacus.validator.BaseInputValidator @@ -18,11 +23,31 @@ internal class TradePositionStateValidator( ) : BaseInputValidator(localizer, formatter, parser), TradeValidatorProtocol { override fun validateTrade( internalState: InternalState, + subaccountNumber: Int?, change: PositionChange, restricted: Boolean, environment: V4Environment? ): List? { - return null + val trade = internalState.input.trade + val subaccountNumber = subaccountNumber ?: return null + val subaccount = internalState.wallet.account.subaccounts[subaccountNumber] + val position = subaccount?.openPositions?.get(trade.marketId) + + val errors = mutableListOf() + validatePositionSize( + position = position, + market = internalState.marketsSummary.markets[trade.marketId], + )?.let { + errors.add(it) + } + validatePositionFlip( + change = change, + trade = trade, + )?.let { + errors.add(it) + } + + return errors } override fun validateTradeDeprecated( @@ -47,19 +72,15 @@ internal class TradePositionStateValidator( } val errors = mutableListOf() - val closeOnlyError = validateCloseOnly( - market, - change, - ) if (position != null) { - val positionSizeError = validatePositionSize( + val positionSizeError = validatePositionSizeDeprecated( position, market, ) if (positionSizeError != null) { errors.add(positionSizeError) } - val positionFlipError = validatePositionFlip( + val positionFlipError = validatePositionFlipDeprecated( change, trade, ) @@ -70,32 +91,34 @@ internal class TradePositionStateValidator( return if (errors.size > 0) errors else null } - private fun validateCloseOnly( - market: Map?, - change: PositionChange, - ): Map? { + private fun validatePositionSize( + position: InternalPerpetualPosition?, + market: InternalMarketState?, + ): ValidationError? { /* - MARKET_STATUS_CLOSE_ONLY + NEW_POSITION_SIZE_OVER_MAX */ - val status = parser.asNativeMap(market?.get("status")) - val marketId = parser.asNativeMap(market?.get("assetId")) ?: "" - val canTrade = parser.asBool(status?.get("canTrade")) ?: false - val canReduce = parser.asBool(status?.get("canReduce")) ?: false - return if (!canTrade && canReduce) { - val isError = when (change) { - PositionChange.CROSSING, PositionChange.NEW, PositionChange.INCREASING -> true - else -> false - } - errorDeprecated( - type = if (isError) "ERROR" else "WARNING", - errorCode = "MARKET_STATUS_CLOSE_ONLY", - fields = if (isError) listOf("size.size") else null, - actionStringKey = if (isError) "APP.TRADE.MODIFY_SIZE_FIELD" else null, - titleStringKey = "WARNINGS.TRADE_BOX_TITLE.MARKET_STATUS_CLOSE_ONLY", - textStringKey = "WARNINGS.TRADE_BOX.MARKET_STATUS_CLOSE_ONLY", + val size = position?.calculated?.get(CalculationPeriod.post)?.size ?: return null + val maxSize = market?.perpetualMarket?.configs?.maxPositionSize ?: Numeric.double.ZERO + if (maxSize == Numeric.double.ZERO) { + return null + } + val symbol = market?.perpetualMarket?.assetId ?: return null + return if (size > maxSize) { + error( + type = ErrorType.error, + errorCode = "NEW_POSITION_SIZE_OVER_MAX", + fields = listOf("size.size"), + actionStringKey = "APP.TRADE.MODIFY_SIZE_FIELD", + titleStringKey = "ERRORS.TRADE_BOX_TITLE.NEW_POSITION_SIZE_OVER_MAX", + textStringKey = "ERRORS.TRADE_BOX.NEW_POSITION_SIZE_OVER_MAX", textParams = mapOf( - "MARKET" to mapOf( - "value" to marketId, + "MAX_SIZE" to mapOf( + "value" to maxSize, + "format" to "size", + ), + "SYMBOL" to mapOf( + "value" to symbol, "format" to "string", ), ), @@ -105,7 +128,7 @@ internal class TradePositionStateValidator( } } - private fun validatePositionSize( + private fun validatePositionSizeDeprecated( position: Map?, market: Map?, ): Map? { @@ -144,6 +167,32 @@ internal class TradePositionStateValidator( } private fun validatePositionFlip( + change: PositionChange, + trade: InternalTradeInputState, + ): ValidationError? { + /* + ORDER_WOULD_FLIP_POSITION + */ + val needsReduceOnly = trade.options.needsReduceOnly + return if (needsReduceOnly && trade.reduceOnly) { + when (change) { + PositionChange.NEW, PositionChange.INCREASING, PositionChange.CROSSING -> error( + type = ErrorType.error, + errorCode = "ORDER_WOULD_FLIP_POSITION", + fields = listOf("size.size"), + actionStringKey = "APP.TRADE.MODIFY_SIZE_FIELD", + titleStringKey = "ERRORS.TRADE_BOX_TITLE.ORDER_WOULD_FLIP_POSITION", + textStringKey = "ERRORS.TRADE_BOX.ORDER_WOULD_FLIP_POSITION", + ) + + else -> null + } + } else { + null + } + } + + private fun validatePositionFlipDeprecated( change: PositionChange, trade: Map, ): Map? { diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeResctrictedValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeResctrictedValidator.kt index c85b2e212..c097c0cc8 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeResctrictedValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeResctrictedValidator.kt @@ -19,6 +19,7 @@ internal class TradeResctrictedValidator( ) : BaseInputValidator(localizer, formatter, parser), TradeValidatorProtocol { override fun validateTrade( internalState: InternalState, + subaccountNumber: Int?, change: PositionChange, restricted: Boolean, environment: V4Environment? diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeTriggerPriceValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeTriggerPriceValidator.kt index 863503475..92e323bec 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeTriggerPriceValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeTriggerPriceValidator.kt @@ -1,10 +1,16 @@ package exchange.dydx.abacus.validator.trade +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.OrderType import exchange.dydx.abacus.output.input.ValidationError import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.app.helper.Formatter import exchange.dydx.abacus.state.internalstate.InternalState +import exchange.dydx.abacus.state.internalstate.InternalSubaccountState +import exchange.dydx.abacus.state.internalstate.InternalTradeInputState import exchange.dydx.abacus.state.manager.V4Environment import exchange.dydx.abacus.validator.BaseInputValidator import exchange.dydx.abacus.validator.PositionChange @@ -16,7 +22,7 @@ enum class RelativeToPrice(val rawValue: String) { companion object { operator fun invoke(rawValue: String) = - values().firstOrNull { it.rawValue == rawValue } + entries.firstOrNull { it.rawValue == rawValue } } } @@ -27,11 +33,101 @@ internal class TradeTriggerPriceValidator( ) : BaseInputValidator(localizer, formatter, parser), TradeValidatorProtocol { override fun validateTrade( internalState: InternalState, + subaccountNumber: Int?, change: PositionChange, restricted: Boolean, environment: V4Environment? ): List? { - return null + val trade = internalState.input.trade + val needsTriggerPrice = trade.options.needsTriggerPrice + if (!needsTriggerPrice) { + return null + } + + val errors = mutableListOf() + val type = trade.type ?: return null + val side = trade.side ?: return null + val subaccountNumber = subaccountNumber ?: return null + val subaccount = internalState.wallet.account.subaccounts[subaccountNumber] + + val market = internalState.marketsSummary.markets[trade.marketId] + val oraclePrice = market?.perpetualMarket?.oraclePrice ?: return null + val triggerPrice = trade.price?.triggerPrice ?: return null + val tickSize = market.perpetualMarket?.configs?.tickSize ?: 0.01 + + when (val triggerToIndex = requiredTriggerToIndexPrice(type, side)) { + /* + TRIGGER_MUST_ABOVE_INDEX_PRICE + TRIGGER_MUST_BELOW_INDEX_PRICE + */ + RelativeToPrice.ABOVE -> { + if (triggerPrice <= oraclePrice) { + errors.add( + triggerToIndexError( + triggerToIndex = triggerToIndex, + type = type, + oraclePrice = oraclePrice, + tickSize = tickSize.toString(), + ), + ) + } + } + + RelativeToPrice.BELOW -> { + if (triggerPrice >= oraclePrice) { + errors.add( + triggerToIndexError( + triggerToIndex = triggerToIndex, + type = type, + oraclePrice = oraclePrice, + tickSize = tickSize.toString(), + ), + ) + } + } + + else -> {} + } + + val triggerToLiquidation = requiredTriggerToLiquidationPrice(type, side, change) + if (triggerToLiquidation != null) { + /* + SELL_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE + BUY_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE + */ + + val subaccount = internalState.wallet.account.subaccounts[subaccountNumber ?: 0] + val liquidationPrice = liquidationPrice(subaccount, trade) + if (liquidationPrice != null) { + when (triggerToLiquidation) { + RelativeToPrice.ABOVE -> { + if (triggerPrice <= liquidationPrice) { + errors.add( + triggerToLiquidationError( + triggerToLiquidation = triggerToLiquidation, + triggerLiquidation = liquidationPrice, + tickSize = tickSize.toString(), + ), + ) + } + } + + RelativeToPrice.BELOW -> { + if (triggerPrice >= liquidationPrice) { + errors.add( + triggerToLiquidationError( + triggerToLiquidation = triggerToLiquidation, + triggerLiquidation = liquidationPrice, + tickSize = tickSize.toString(), + ), + ) + } + } + } + } + } + + return errors } /* @@ -66,7 +162,7 @@ internal class TradeTriggerPriceValidator( val triggerPrice = parser.asDouble(parser.value(trade, "price.triggerPrice")) ?: return null val tickSize = parser.asString(parser.value(market, "configs.tickSize")) ?: "0.01" - when (val triggerToIndex = requiredTriggerToIndexPrice(type, side)) { + when (val triggerToIndex = requiredTriggerToIndexPriceDeprecated(type, side)) { /* TRIGGER_MUST_ABOVE_INDEX_PRICE TRIGGER_MUST_BELOW_INDEX_PRICE @@ -74,7 +170,7 @@ internal class TradeTriggerPriceValidator( RelativeToPrice.ABOVE -> { if (triggerPrice <= oraclePrice) { errors.add( - triggerToIndexError( + triggerToIndexErrorDeprecated( triggerToIndex, type, oraclePrice, @@ -87,7 +183,7 @@ internal class TradeTriggerPriceValidator( RelativeToPrice.BELOW -> { if (triggerPrice >= oraclePrice) { errors.add( - triggerToIndexError( + triggerToIndexErrorDeprecated( triggerToIndex, type, oraclePrice, @@ -99,19 +195,19 @@ internal class TradeTriggerPriceValidator( else -> {} } - val triggerToLiquidation = requiredTriggerToLiquidationPrice(type, side, change) + val triggerToLiquidation = requiredTriggerToLiquidationPriceDeprecated(type, side, change) if (triggerToLiquidation != null) { /* SELL_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE BUY_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE */ - val liquidationPrice = liquidationPrice(subaccount, trade) + val liquidationPrice = liquidationPriceDeprecated(subaccount, trade) if (liquidationPrice != null) { when (triggerToLiquidation) { RelativeToPrice.ABOVE -> { if (triggerPrice <= liquidationPrice) { errors.add( - triggerToLiquidationError( + triggerToLiquidationErrorDeprecated( triggerToLiquidation, liquidationPrice, tickSize, @@ -123,7 +219,7 @@ internal class TradeTriggerPriceValidator( RelativeToPrice.BELOW -> { if (triggerPrice >= liquidationPrice) { errors.add( - triggerToLiquidationError( + triggerToLiquidationErrorDeprecated( triggerToLiquidation, liquidationPrice, tickSize, @@ -139,7 +235,28 @@ internal class TradeTriggerPriceValidator( return null } - private fun requiredTriggerToIndexPrice(type: String, side: String): RelativeToPrice? { + private fun requiredTriggerToIndexPrice( + type: OrderType, + side: OrderSide + ): RelativeToPrice? { + return when (type) { + OrderType.StopLimit, OrderType.StopMarket, OrderType.TrailingStop -> + when (side) { + OrderSide.Buy -> RelativeToPrice.ABOVE + OrderSide.Sell -> RelativeToPrice.BELOW + } + + OrderType.TakeProfitLimit, OrderType.TakeProfitMarket -> + when (side) { + OrderSide.Buy -> RelativeToPrice.BELOW + OrderSide.Sell -> RelativeToPrice.ABOVE + } + + else -> null + } + } + + private fun requiredTriggerToIndexPriceDeprecated(type: String, side: String): RelativeToPrice? { return when (type) { "STOP_LIMIT", "STOP_MARKET", "TRAILING_STOP" -> when (side) { @@ -160,6 +277,53 @@ internal class TradeTriggerPriceValidator( } private fun triggerToIndexError( + triggerToIndex: RelativeToPrice, + type: OrderType, + oraclePrice: Double, + tickSize: String, + ): ValidationError { + val fields = if (type == OrderType.TrailingStop) { + listOf("price.trailingPercent") + } else { + listOf("price.triggerPrice") + } + val action = if (type == OrderType.TrailingStop) { + "APP.TRADE.MODIFY_TRAILING_PERCENT" + } else { + "APP.TRADE.MODIFY_TRIGGER_PRICE" + } + val params = mapOf( + "INDEX_PRICE" to + mapOf( + "value" to oraclePrice, + "format" to "price", + "tickSize" to tickSize, + ), + ) + return when (triggerToIndex) { + RelativeToPrice.ABOVE -> error( + type = ErrorType.error, + errorCode = "TRIGGER_MUST_ABOVE_INDEX_PRICE", + fields = fields, + actionStringKey = action, + titleStringKey = "ERRORS.TRADE_BOX_TITLE.TRIGGER_MUST_ABOVE_INDEX_PRICE", + textStringKey = "ERRORS.TRADE_BOX.TRIGGER_MUST_ABOVE_INDEX_PRICE", + textParams = params, + ) + + RelativeToPrice.BELOW -> error( + type = ErrorType.error, + errorCode = "TRIGGER_MUST_BELOW_INDEX_PRICE", + fields = fields, + actionStringKey = action, + titleStringKey = "ERRORS.TRADE_BOX_TITLE.TRIGGER_MUST_BELOW_INDEX_PRICE", + textStringKey = "ERRORS.TRADE_BOX.TRIGGER_MUST_BELOW_INDEX_PRICE", + textParams = params, + ) + } + } + + private fun triggerToIndexErrorDeprecated( triggerToIndex: RelativeToPrice, type: String, oraclePrice: Double, @@ -209,6 +373,29 @@ internal class TradeTriggerPriceValidator( } private fun requiredTriggerToLiquidationPrice( + type: OrderType, + side: OrderSide, + change: PositionChange, + ): RelativeToPrice? { + return when (type) { + OrderType.StopMarket -> + when (change) { + PositionChange.CLOSING, PositionChange.DECREASING, PositionChange.CROSSING -> { + when (side) { + OrderSide.Sell -> RelativeToPrice.ABOVE + OrderSide.Buy -> RelativeToPrice.BELOW + else -> null + } + } + + else -> null + } + + else -> null + } + } + + private fun requiredTriggerToLiquidationPriceDeprecated( type: String, side: String, change: PositionChange, @@ -232,6 +419,15 @@ internal class TradeTriggerPriceValidator( } private fun liquidationPrice( + subaccount: InternalSubaccountState?, + trade: InternalTradeInputState, + ): Double? { + val marketId = trade.marketId ?: return null + val position = subaccount?.openPositions?.get(marketId) ?: return null + return position.calculated[CalculationPeriod.current]?.liquidationPrice + } + + private fun liquidationPriceDeprecated( subaccount: Map?, trade: Map, ): Double? { @@ -248,6 +444,45 @@ internal class TradeTriggerPriceValidator( triggerToLiquidation: RelativeToPrice, triggerLiquidation: Double, tickSize: String, + ): ValidationError { + val fields = listOf("price.triggerPrice") + val action = "APP.TRADE.MODIFY_TRIGGER_PRICE" + // Localizations uses TRIGGER_PRICE_LIMIT as paramater name + val params = + mapOf( + "TRIGGER_PRICE_LIMIT" to mapOf( + "value" to triggerLiquidation, + "format" to "price", + "tickSize" to tickSize, + ), + ) + return when (triggerToLiquidation) { + RelativeToPrice.ABOVE -> error( + type = ErrorType.error, + errorCode = "SELL_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE", + fields = fields, + actionStringKey = action, + titleStringKey = "ERRORS.TRADE_BOX_TITLE.SELL_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE", + textStringKey = "ERRORS.TRADE_BOX.SELL_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE", + textParams = params, + ) + + RelativeToPrice.BELOW -> error( + type = ErrorType.error, + errorCode = "BUY_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE", + fields = fields, + actionStringKey = action, + titleStringKey = "ERRORS.TRADE_BOX_TITLE.BUY_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE", + textStringKey = "ERRORS.TRADE_BOX.BUY_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE", + textParams = params, + ) + } + } + + private fun triggerToLiquidationErrorDeprecated( + triggerToLiquidation: RelativeToPrice, + triggerLiquidation: Double, + tickSize: String, ): Map { val fields = listOf("price.triggerPrice") val action = "APP.TRADE.MODIFY_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 455cb0615..bcf5b838d 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/transfer/WithdrawalCapacityValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/transfer/WithdrawalCapacityValidator.kt @@ -24,7 +24,7 @@ internal class WithdrawalCapacityValidator( restricted: Boolean, environment: V4Environment? ): List? { - TODO("Not yet implemented") + return null } override fun validateTransferDeprecated( From 07b3cfb3b87fe70c83c364ee7b71c98aa5a8bfa6 Mon Sep 17 00:00:00 2001 From: Rui Date: Wed, 21 Aug 2024 05:16:40 +0900 Subject: [PATCH 49/63] Clean up --- .../calculator/V2/TradeInput/TradeInputOptionsCalculator.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputOptionsCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputOptionsCalculator.kt index 6f52a0200..c24596193 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputOptionsCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputOptionsCalculator.kt @@ -408,8 +408,7 @@ internal class TradeInputOptionsCalculator( private fun trailingPercentField(): Map { return mapOf( - "" + - "field" to "price.trailingPercent", + "field" to "price.trailingPercent", "type" to "double", ) } From 6227a52410be5e5ad29514045a7e39be2f8870f2 Mon Sep 17 00:00:00 2001 From: Rui Date: Wed, 21 Aug 2024 05:39:24 +0900 Subject: [PATCH 50/63] Clean up --- .../state/model/TradingStateMachine+TradeInput.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt index 0d9e55261..51ec91d80 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt @@ -19,10 +19,6 @@ import kollections.JsExport import kollections.iListOf import kotlinx.serialization.Serializable -internal interface InputFieldProtocol { - val test: ((InternalTradeInputState) -> Any?)? -} - @JsExport @Serializable enum class TradeInputField(val rawValue: String) { From fbaf6977df43204ade40e2d8d9c4f9af43ccf9e9 Mon Sep 17 00:00:00 2001 From: mobile-build-bot-git Date: Tue, 20 Aug 2024 20:49:36 +0000 Subject: [PATCH 51/63] Bump version --- build.gradle.kts | 2 +- v4_abacus.podspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index fff421617..635727b99 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -52,7 +52,7 @@ allprojects { } group = "exchange.dydx.abacus" -version = "1.8.94" +version = "1.8.95" repositories { google() diff --git a/v4_abacus.podspec b/v4_abacus.podspec index ff7e40d78..3820a4911 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.94' + spec.version = '1.8.95' spec.homepage = 'https://github.com/dydxprotocol/v4-abacus' spec.source = { :http=> ''} spec.authors = '' From 8c1def95b10293a7e7a5ca6aea531d900a4b0cd3 Mon Sep 17 00:00:00 2001 From: Rui Date: Wed, 21 Aug 2024 07:48:25 +0900 Subject: [PATCH 52/63] TradeFieldsValidator --- .../TradeInput/TradeInputOptionsCalculator.kt | 6 +- .../processor/input/TradeInputProcessor.kt | 4 +- .../validator/TradeInputValidator.kt | 2 + .../validator/trade/TradeFieldsValidator.kt | 134 ++++++++++++++++++ 4 files changed, 142 insertions(+), 4 deletions(-) create mode 100644 src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeFieldsValidator.kt diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputOptionsCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputOptionsCalculator.kt index c24596193..63267028a 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputOptionsCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputOptionsCalculator.kt @@ -273,7 +273,8 @@ internal class TradeInputOptionsCalculator( market = market, ) if (options.executionOptions != null) { - if (trade.execution == null) { + val types = options.executionOptions?.map { it.type } + if (trade.execution == null || types?.contains(trade.execution) == false) { trade.execution = options.executionOptions?.firstOrNull()?.type } } @@ -286,7 +287,8 @@ internal class TradeInputOptionsCalculator( market = market, ) if (options.marginModeOptions != null) { - if (trade.marginMode == null) { + val types = options.marginModeOptions?.map { MarginMode.invoke(it.type) } + if (trade.marginMode == null || types?.contains(trade.marginMode) == false) { trade.marginMode = MarginMode.invoke(options.marginModeOptions?.firstOrNull()?.type) } } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputProcessor.kt index fbf5ccf94..973f7a2f0 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputProcessor.kt @@ -181,8 +181,8 @@ internal class TradeInputProcessor( ) } else { error = ParsingError( - ParsingErrorType.MissingRequiredData, - "$inputData is not a valid string", + type = ParsingErrorType.MissingRequiredData, + message = "$inputData is not a valid string", ) } } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/TradeInputValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/TradeInputValidator.kt index 22137cd57..ef03e9fc0 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/TradeInputValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/TradeInputValidator.kt @@ -14,6 +14,7 @@ import exchange.dydx.abacus.state.manager.V4Environment import exchange.dydx.abacus.utils.Numeric import exchange.dydx.abacus.validator.trade.TradeAccountStateValidator import exchange.dydx.abacus.validator.trade.TradeBracketOrdersValidator +import exchange.dydx.abacus.validator.trade.TradeFieldsValidator import exchange.dydx.abacus.validator.trade.TradeInputDataValidator import exchange.dydx.abacus.validator.trade.TradeMarketOrderInputValidator import exchange.dydx.abacus.validator.trade.TradePositionStateValidator @@ -26,6 +27,7 @@ internal class TradeInputValidator( parser: ParserProtocol ) : BaseInputValidator(localizer, formatter, parser), ValidatorProtocol { private val tradeValidators = listOf( + TradeFieldsValidator(localizer, formatter, parser), TradeResctrictedValidator(localizer, formatter, parser), TradeInputDataValidator(localizer, formatter, parser), TradeMarketOrderInputValidator(localizer, formatter, parser), diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeFieldsValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeFieldsValidator.kt new file mode 100644 index 000000000..3fb30c091 --- /dev/null +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeFieldsValidator.kt @@ -0,0 +1,134 @@ +package exchange.dydx.abacus.validator.trade + +import exchange.dydx.abacus.output.input.ValidationError +import exchange.dydx.abacus.protocols.LocalizerProtocol +import exchange.dydx.abacus.protocols.ParserProtocol +import exchange.dydx.abacus.state.app.helper.Formatter +import exchange.dydx.abacus.state.internalstate.InternalState +import exchange.dydx.abacus.state.manager.V4Environment +import exchange.dydx.abacus.state.model.TradeInputField +import exchange.dydx.abacus.validator.BaseInputValidator +import exchange.dydx.abacus.validator.PositionChange +import exchange.dydx.abacus.validator.TradeValidatorProtocol +import kotlin.time.Duration + +internal class TradeFieldsValidator( + localizer: LocalizerProtocol?, + formatter: Formatter?, + parser: ParserProtocol, +) : BaseInputValidator(localizer, formatter, parser), TradeValidatorProtocol { + override fun validateTrade( + internalState: InternalState, + subaccountNumber: Int?, + change: PositionChange, + restricted: Boolean, + environment: V4Environment? + ): List? { + val trade = internalState.input.trade + + val errors = mutableListOf() + if (trade.options.needsSize) { + val size = trade.size?.size ?: 0.0 + val usdcSize = trade.size?.usdcSize ?: 0.0 + if (size == 0.0 && usdcSize == 0.0) { + errors.add( + required( + errorCode = "REQUIRED_SIZE", + field = if (size == 0.0) TradeInputField.size.rawValue else TradeInputField.usdcSize.rawValue, + actionStringKey = "APP.TRADE.ENTER_AMOUNT", + ), + ) + } + } + + if (trade.options.needsTriggerPrice) { + val triggerPrice = trade.price?.triggerPrice ?: 0.0 + if (triggerPrice == 0.0) { + errors.add( + required( + errorCode = "REQUIRED_TRIGGER_PRICE", + field = TradeInputField.triggerPrice.rawValue, + actionStringKey = "APP.TRADE.ENTER_TRIGGER_PRICE", + ), + ) + } + } + + if (trade.options.needsLimitPrice) { + val limitPrice = trade.price?.limitPrice ?: 0.0 + if (limitPrice == 0.0) { + errors.add( + required( + errorCode = "REQUIRED_LIMIT_PRICE", + field = TradeInputField.limitPrice.rawValue, + actionStringKey = "APP.TRADE.ENTER_LIMIT_PRICE", + ), + ) + } + } + + if (trade.options.needsTrailingPercent) { + val trailingPercent = trade.price?.trailingPercent ?: 0.0 + if (trailingPercent == 0.0) { + errors.add( + required( + errorCode = "REQUIRED_TRAILING_PERCENT", + field = TradeInputField.trailingPercent.rawValue, + actionStringKey = "APP.TRADE.ENTER_TRAILING_PERCENT", + ), + ) + } + } + + if (!trade.options.timeInForceOptions.isNullOrEmpty()) { + if (trade.timeInForce.isNullOrEmpty()) { + errors.add( + required( + errorCode = "REQUIRED_TIME_IN_FORCE", + field = TradeInputField.timeInForceType.rawValue, + actionStringKey = "APP.TRADE.ENTER_TIME_IN_FORCE", + ), + ) + } + } + + if (trade.options.needsGoodUntil) { + val goodTil = trade.goodTil?.timeInterval ?: Duration.ZERO + if (goodTil == Duration.ZERO) { + errors.add( + required( + errorCode = "REQUIRED_GOOD_UNTIL", + field = "goodTil", + actionStringKey = "APP.TRADE.ENTER_GOOD_UNTIL", + ), + ) + } + } + + if (!trade.options.executionOptions.isNullOrEmpty()) { + if (trade.execution.isNullOrEmpty()) { + errors.add( + required( + errorCode = "REQUIRED_EXECUTION", + field = TradeInputField.execution.rawValue, + actionStringKey = "APP.TRADE.ENTER_EXECUTION", + ), + ) + } + } + + return errors + } + + override fun validateTradeDeprecated( + subaccount: Map?, + market: Map?, + configs: Map?, + trade: Map, + change: PositionChange, + restricted: Boolean, + environment: V4Environment? + ): List? { + return null + } +} From 0a2bd04e032405edccbb8ed20f032dd200c8ae00 Mon Sep 17 00:00:00 2001 From: Rui Date: Thu, 22 Aug 2024 07:11:16 +0800 Subject: [PATCH 53/63] AccountInputValidator --- .../state/internalstate/InternalState.kt | 8 +- .../validator/AccountInputValidator.kt | 85 ++++++++++++++++++- 2 files changed, 90 insertions(+), 3 deletions(-) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt index 72449648c..d522f7e3f 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt @@ -183,7 +183,13 @@ internal data class InternalWalletState( var account: InternalAccountState = InternalAccountState(), var user: InternalUserState? = null, var walletAddress: String? = null, -) +) { + val isWalletConnected: Boolean + get() = walletAddress != null + + val isAccountConnected: Boolean + get() = account.subaccounts != null +} internal data class InternalUserState( var feeTierId: String? = null, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/AccountInputValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/AccountInputValidator.kt index 25251765d..e27c6139f 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/AccountInputValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/AccountInputValidator.kt @@ -1,11 +1,15 @@ package exchange.dydx.abacus.validator +import exchange.dydx.abacus.calculator.CalculationPeriod +import exchange.dydx.abacus.output.input.ErrorAction +import exchange.dydx.abacus.output.input.ErrorType import exchange.dydx.abacus.output.input.InputType import exchange.dydx.abacus.output.input.ValidationError import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol import exchange.dydx.abacus.state.app.helper.Formatter import exchange.dydx.abacus.state.internalstate.InternalState +import exchange.dydx.abacus.state.internalstate.InternalWalletState import exchange.dydx.abacus.state.manager.BlockAndTime import exchange.dydx.abacus.state.manager.V4Environment import exchange.dydx.abacus.utils.NUM_PARENT_SUBACCOUNTS @@ -22,7 +26,18 @@ internal class AccountInputValidator( inputType: InputType, environment: V4Environment?, ): List? { - return null + val error = + missingWallet( + wallet = internalState.wallet, + ) + ?: missingAccount( + wallet = internalState.wallet, + ) + ?: checkEquity( + wallet = internalState.wallet, + subaccountNumber = subaccountNumber, + ) + return if (error != null) listOf(error) else null } override fun validateDeprecated( @@ -36,7 +51,7 @@ internal class AccountInputValidator( transactionType: String, environment: V4Environment?, ): List? { - val error = missingWallet(parser, wallet) ?: missingAccount(parser, wallet) ?: checkEquity( + val error = missingWalletDeprecated(parser, wallet) ?: missingAccountDeprecated(parser, wallet) ?: checkEquityDeprecated( parser, subaccount, ) @@ -44,6 +59,25 @@ internal class AccountInputValidator( } private fun missingWallet( + wallet: InternalWalletState, + ): ValidationError? { + return if (wallet.isWalletConnected) { + null + } else { + error( + type = ErrorType.error, + errorCode = "REQUIRED_WALLET", + fields = null, + actionStringKey = "ERRORS.TRADE_BOX_TITLE.CONNECT_WALLET_TO_TRADE", + titleStringKey = "ERRORS.TRADE_BOX_TITLE.CONNECT_WALLET_TO_TRADE", + textStringKey = "ERRORS.TRADE_BOX.CONNECT_WALLET_TO_TRADE", + textParams = null, + action = ErrorAction.CONNECT_WALLET, + ) + } + } + + private fun missingWalletDeprecated( parser: ParserProtocol, wallet: Map?, ): Map? { @@ -64,6 +98,25 @@ internal class AccountInputValidator( } private fun missingAccount( + wallet: InternalWalletState, + ): ValidationError? { + return if (wallet.isAccountConnected) { + null + } else { + error( + type = ErrorType.error, + errorCode = "REQUIRED_ACCOUNT", + fields = null, + actionStringKey = "ERRORS.TRADE_BOX_TITLE.DEPOSIT_TO_TRADE", + titleStringKey = "ERRORS.TRADE_BOX_TITLE.DEPOSIT_TO_TRADE", + textStringKey = "ERRORS.TRADE_BOX.DEPOSIT_TO_TRADE", + textParams = null, + action = ErrorAction.DEPOSIT, + ) + } + } + + private fun missingAccountDeprecated( parser: ParserProtocol, wallet: Map?, ): Map? { @@ -85,6 +138,34 @@ internal class AccountInputValidator( } private fun checkEquity( + wallet: InternalWalletState, + subaccountNumber: Int?, + ): ValidationError? { + val isChildSubaccountForIsolatedMargin = subaccountNumber == null || subaccountNumber >= NUM_PARENT_SUBACCOUNTS + val subaccount = wallet.account.subaccounts[subaccountNumber] + val equity = subaccount?.calculated?.get(CalculationPeriod.current)?.equity + + return if (equity != null && equity > 0) { + null + } else if (isChildSubaccountForIsolatedMargin) { + // Equity is null when a user is placing an Isolated Margin trade on a childSubaccount + // subaccountNumber is null when a childSubaccount has not been created yet + null + } else { + error( + type = ErrorType.error, + errorCode = "NO_EQUITY_DEPOSIT_FIRST", + fields = null, + actionStringKey = "ERRORS.TRADE_BOX_TITLE.NO_EQUITY_DEPOSIT_FIRST", + titleStringKey = "ERRORS.TRADE_BOX_TITLE.NO_EQUITY_DEPOSIT_FIRST", + textStringKey = "ERRORS.TRADE_BOX.NO_EQUITY_DEPOSIT_FIRST", + textParams = null, + action = ErrorAction.DEPOSIT, + ) + } + } + + private fun checkEquityDeprecated( parser: ParserProtocol, subaccount: Map?, ): Map? { From 270b53b872ce70b17bf12f4f10154df1beb12731 Mon Sep 17 00:00:00 2001 From: Rui Date: Thu, 22 Aug 2024 10:14:28 +0800 Subject: [PATCH 54/63] Update integration tests --- .../output/account/Account.kt | 82 ++++-- .../output/account/Subaccount.kt | 46 ++-- .../output/input/Input.kt | 3 + .../exchange.dydx.abacus/payload/BaseTests.kt | 240 +++++++++++++++++- .../v3/V3TradeInputWithoutAccountTests.kt | 45 +++- 5 files changed, 345 insertions(+), 71 deletions(-) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/output/account/Account.kt b/src/commonMain/kotlin/exchange.dydx.abacus/output/account/Account.kt index 54e766957..39bacad42 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/output/account/Account.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/output/account/Account.kt @@ -162,46 +162,76 @@ data class Account( val subaccounts: IMutableMap = iMutableMapOf() - val subaccountsData = parser.asMap(data["subaccounts"]) - if (subaccountsData != null) { - for ((key, value) in subaccountsData) { - val subaccountData = parser.asMap(value) ?: iMapOf() - - val subaccountNumber = parser.asInt(key) ?: 0 + if (staticTyping) { + internalState.subaccounts.forEach { (key, value) -> Subaccount.create( - existing = existing?.subaccounts?.get(key), + existing = existing?.subaccounts?.get(key.toString()), parser = parser, - data = subaccountData, + data = null, localizer = localizer, staticTyping = staticTyping, - internalState = internalState.subaccounts[subaccountNumber], - ) - ?.let { subaccount -> - subaccounts[key] = subaccount - } + internalState = value, + )?.let { subaccount -> + subaccounts[key.toString()] = subaccount + } + } + } else { + val subaccountsData = parser.asMap(data["subaccounts"]) + if (subaccountsData != null) { + for ((key, value) in subaccountsData) { + val subaccountData = parser.asMap(value) ?: iMapOf() + + val subaccountNumber = parser.asInt(key) ?: 0 + Subaccount.create( + existing = existing?.subaccounts?.get(key), + parser = parser, + data = subaccountData, + localizer = localizer, + staticTyping = staticTyping, + internalState = internalState.subaccounts[subaccountNumber], + ) + ?.let { subaccount -> + subaccounts[key] = subaccount + } + } } } val groupedSubaccounts: IMutableMap = iMutableMapOf() - val groupedSubaccountsData = parser.asMap(data["groupedSubaccounts"]) - if (groupedSubaccountsData != null) { - for ((key, value) in groupedSubaccountsData) { - val subaccountData = parser.asMap(value) ?: iMapOf() - - val subaccountNumber = parser.asInt(key) ?: 0 + if (staticTyping) { + internalState.groupedSubaccounts.forEach { (key, value) -> Subaccount.create( - existing = existing?.subaccounts?.get(key), + existing = existing?.groupedSubaccounts?.get(key.toString()), parser = parser, - data = subaccountData, + data = null, localizer = localizer, staticTyping = staticTyping, - internalState = internalState.subaccounts[subaccountNumber], - ) - ?.let { subaccount -> - groupedSubaccounts[key] = subaccount - } + internalState = value, + )?.let { subaccount -> + groupedSubaccounts[key.toString()] = subaccount + } + } + } else { + val groupedSubaccountsData = parser.asMap(data["groupedSubaccounts"]) + if (groupedSubaccountsData != null) { + for ((key, value) in groupedSubaccountsData) { + val subaccountData = parser.asMap(value) ?: iMapOf() + + val subaccountNumber = parser.asInt(key) ?: 0 + Subaccount.create( + existing = existing?.subaccounts?.get(key), + parser = parser, + data = subaccountData, + localizer = localizer, + staticTyping = staticTyping, + internalState = internalState.subaccounts[subaccountNumber], + ) + ?.let { subaccount -> + groupedSubaccounts[key] = subaccount + } + } } } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/output/account/Subaccount.kt b/src/commonMain/kotlin/exchange.dydx.abacus/output/account/Subaccount.kt index 76da5ff37..d99df32bb 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/output/account/Subaccount.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/output/account/Subaccount.kt @@ -57,12 +57,13 @@ data class Subaccount( Logger.d { "Internal state is null" } return null } - data?.let { - val positionId = if (staticTyping) null else parser.asString(data["positionId"]) - val pnlTotal = if (staticTyping) null else parser.asDouble(data["pnlTotal"]) - val pnl24h = if (staticTyping) null else parser.asDouble(data["pnl24h"]) + + if (staticTyping || data != null) { + val positionId = if (staticTyping) null else parser.asString(data?.get("positionId")) + val pnlTotal = if (staticTyping) null else parser.asDouble(data?.get("pnlTotal")) + val pnl24h = if (staticTyping) null else parser.asDouble(data?.get("pnl24h")) val pnl24hPercent = - if (staticTyping) null else parser.asDouble(data["pnl24hPercent"]) + if (staticTyping) null else parser.asDouble(data?.get("pnl24hPercent")) /* val historicalPnl = (data["historicalPnl"] as? List<*>)?.let { val historicalPnl = iMutableListOf() @@ -82,7 +83,7 @@ data class Subaccount( val subaccountNumber = if (staticTyping) { internalState?.subaccountNumber ?: 0 } else { - parser.asInt(data["subaccountNumber"]) ?: 0 + parser.asInt(data?.get("subaccountNumber")) ?: 0 } val quoteBalance = if (staticTyping) { @@ -95,7 +96,7 @@ data class Subaccount( TradeStatesWithDoubleValues.create( existing?.quoteBalance, parser, - parser.asMap(data["quoteBalance"]), + parser.asMap(data?.get("quoteBalance")), ) } @@ -109,7 +110,7 @@ data class Subaccount( TradeStatesWithDoubleValues.create( existing?.notionalTotal, parser, - parser.asMap(data["notionalTotal"]), + parser.asMap(data?.get("notionalTotal")), ) } @@ -123,7 +124,7 @@ data class Subaccount( TradeStatesWithDoubleValues.create( existing?.valueTotal, parser, - parser.asMap(data["valueTotal"]), + parser.asMap(data?.get("valueTotal")), ) } @@ -137,7 +138,7 @@ data class Subaccount( TradeStatesWithDoubleValues.create( existing?.initialRiskTotal, parser, - parser.asMap(data["initialRiskTotal"]), + parser.asMap(data?.get("initialRiskTotal")), ) } @@ -152,7 +153,7 @@ data class Subaccount( TradeStatesWithDoubleValues.create( existing?.adjustedImf, parser, - parser.asMap(data["adjustedImf"]), + parser.asMap(data?.get("adjustedImf")), ) } @@ -166,7 +167,7 @@ data class Subaccount( TradeStatesWithDoubleValues.create( existing?.equity, parser, - parser.asMap(data["equity"]), + parser.asMap(data?.get("equity")), ) } @@ -180,7 +181,7 @@ data class Subaccount( TradeStatesWithDoubleValues.create( existing?.freeCollateral, parser, - parser.asMap(data["freeCollateral"]), + parser.asMap(data?.get("freeCollateral")), ) } @@ -194,7 +195,7 @@ data class Subaccount( TradeStatesWithDoubleValues.create( existing?.leverage, parser, - parser.asMap(data["leverage"]), + parser.asMap(data?.get("leverage")), ) } @@ -208,7 +209,7 @@ data class Subaccount( TradeStatesWithDoubleValues.create( existing?.marginUsage, parser, - parser.asMap(data["marginUsage"]), + parser.asMap(data?.get("marginUsage")), ) } @@ -222,7 +223,7 @@ data class Subaccount( TradeStatesWithDoubleValues.create( existing?.buyingPower, parser, - parser.asMap(data["buyingPower"]), + parser.asMap(data?.get("buyingPower")), ) } @@ -237,20 +238,20 @@ data class Subaccount( openPositionsDeprecated( existing = existing?.openPositions, parser = parser, - data = parser.asMap(data["openPositions"]), + data = parser.asMap(data?.get("openPositions")), ) } val pendingPositions = pendingPositions( existing?.pendingPositions, parser, - parser.asList(data["pendingPositions"]), + parser.asList(data?.get("pendingPositions")), ) val orders = if (staticTyping) { internalState?.orders?.toIList() } else { - orders(parser, existing?.orders, parser.asMap(data["orders"]), localizer) + orders(parser, existing?.orders, parser.asMap(data?.get("orders")), localizer) } /* @@ -268,7 +269,7 @@ data class Subaccount( val marginEnabled = if (staticTyping) { internalState?.marginEnabled ?: true } else { - parser.asBool(data["marginEnabled"]) ?: true + parser.asBool(data?.get("marginEnabled")) ?: true } return if (existing?.subaccountNumber != subaccountNumber || @@ -324,7 +325,7 @@ data class Subaccount( parser: ParserProtocol, openPositions: Map?, subaccount: InternalSubaccountState?, - ): IList { + ): IList? { val newEntries: MutableList = mutableListOf() for ((key, value) in openPositions?.entries ?: emptySet()) { val position = SubaccountPosition.create( @@ -340,6 +341,9 @@ data class Subaccount( } } newEntries.sortByDescending { it.createdAtMilliseconds } + if (newEntries.isEmpty()) { + return null + } return if (newEntries != existing) { newEntries.toIList() } else { diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/output/input/Input.kt b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/Input.kt index db3c2fabb..aae4f1e6a 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/output/input/Input.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/Input.kt @@ -47,6 +47,9 @@ data class Input( staticTyping: Boolean, ): Input? { Logger.d { "creating Input\n" } + if (staticTyping && internalState?.input?.currentType == null) { + return null + } if (staticTyping || data != null) { val current = if (staticTyping) { diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/payload/BaseTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/payload/BaseTests.kt index 3a3321b78..dec1ef03e 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/payload/BaseTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/payload/BaseTests.kt @@ -6,6 +6,7 @@ import exchange.dydx.abacus.app.manager.TestRest import exchange.dydx.abacus.app.manager.TestThreading import exchange.dydx.abacus.app.manager.TestTimer import exchange.dydx.abacus.app.manager.TestWebSocket +import exchange.dydx.abacus.calculator.CalculationPeriod import exchange.dydx.abacus.output.Asset import exchange.dydx.abacus.output.Configs import exchange.dydx.abacus.output.FeeDiscount @@ -40,6 +41,7 @@ import exchange.dydx.abacus.output.account.SubaccountTransfer import exchange.dydx.abacus.output.input.ClosePositionInput import exchange.dydx.abacus.output.input.ClosePositionInputSize import exchange.dydx.abacus.output.input.Input +import exchange.dydx.abacus.output.input.InputType import exchange.dydx.abacus.output.input.MarginMode import exchange.dydx.abacus.output.input.OrderbookUsage import exchange.dydx.abacus.output.input.ReceiptLine @@ -53,13 +55,18 @@ import exchange.dydx.abacus.output.input.TradeInputOptions import exchange.dydx.abacus.output.input.TradeInputPrice import exchange.dydx.abacus.output.input.TradeInputSize import exchange.dydx.abacus.output.input.TradeInputSummary +import exchange.dydx.abacus.processor.utils.MarketId import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.responses.StateResponse import exchange.dydx.abacus.state.app.helper.DynamicLocalizer import exchange.dydx.abacus.state.internalstate.InternalAccountState import exchange.dydx.abacus.state.internalstate.InternalConfigsState +import exchange.dydx.abacus.state.internalstate.InternalPerpetualPosition import exchange.dydx.abacus.state.internalstate.InternalState import exchange.dydx.abacus.state.internalstate.InternalSubaccountState +import exchange.dydx.abacus.state.internalstate.InternalTradeInputOptions +import exchange.dydx.abacus.state.internalstate.InternalTradeInputState +import exchange.dydx.abacus.state.internalstate.InternalTradeInputSummary import exchange.dydx.abacus.state.model.PerpTradingStateMachine import exchange.dydx.abacus.state.model.TradingStateMachine import exchange.dydx.abacus.tests.payloads.AbacusMockData @@ -317,16 +324,58 @@ open class BaseTests( "orderbooks", ) } - verifyInputState(perp.input, state?.input, "input") + if (staticTyping) { + perp.internalState.input + } else { + verifyInputStateDeprecated(perp.input, state?.input, "input") + } } - private fun verifyInputState(data: Map?, obj: Input?, trace: String) { + private fun verifyInputState( + data: InternalState?, + obj: Input?, + trace: String + ) { + if (data != null) { + assertNotNull(obj) + assertEquals(data.input.currentType, obj.current, "$trace.current") + when (obj.current) { + InputType.TRADE -> { + verifyInputTradeState( + data = data.input.trade, + obj = obj.trade, + trace = "$trace.trade", + ) + } + + // TODO: Close position + InputType.CLOSE_POSITION -> { +// verifyInputClosePositionState( +// data.input.closePosition, +// obj.closePosition, +// "$trace.closePosition", +// ) + } + + InputType.TRANSFER -> {} + InputType.TRIGGER_ORDERS -> {} + InputType.ADJUST_ISOLATED_MARGIN -> {} + null -> {} + } + + assertEquals(data.input.receiptLines, obj.receiptLines, "$trace.receiptLines") + } else { + assertNull(obj) + } + } + + private fun verifyInputStateDeprecated(data: Map?, obj: Input?, trace: String) { if (data != null) { assertNotNull(obj) assertEquals(parser.asString(data["current"]), obj.current?.rawValue, "$trace.current") when (obj.current?.rawValue) { "trade" -> { - verifyInputTradeState( + verifyInputTradeStateDeprecated( parser.asNativeMap(data["trade"]), obj.trade, "$trace.trade", @@ -352,7 +401,41 @@ open class BaseTests( } } - private fun verifyInputTradeState(data: Map?, obj: TradeInput?, trace: String) { + private fun verifyInputTradeState( + data: InternalTradeInputState?, + obj: TradeInput?, + trace: String + ) { + if (data != null) { + assertNotNull(obj, "$trace should not be null") + assertEquals(data.type, obj.type, "$trace.type $doesntMatchText") + assertEquals(data.side, obj.side, "$trace.side $doesntMatchText") + assertEquals(data.marketId, obj.marketId, "$trace.marketId $doesntMatchText") + assertEquals(data.execution, obj.execution, "$trace.execution $doesntMatchText") + assertEquals(data.timeInForce, obj.timeInForce, "$trace.timeInForce $doesntMatchText") + assertEquals(data.postOnly, obj.postOnly, "$trace.postOnly $doesntMatchText") + assertEquals(data.reduceOnly, obj.reduceOnly, "$trace.reduceOnly $doesntMatchText") + assertEquals(data.marginMode, obj.marginMode, "$trace.marginMode $doesntMatchText") + assertEquals(data.size, obj.size, "$trace.size $doesntMatchText") + assertEquals(data.goodTil, obj.goodTil, "$trace.goodTil $doesntMatchText") + assertEquals(data.marketOrder, obj.marketOrder, "$trace.marketOrder $doesntMatchText") + verifyInputTradeInputOptionsState( + data = data.options, + obj = obj.options, + trace = "$trace.options", + ) + verifyInputTradeInputSummaryState( + data = data.summary, + obj = obj.summary, + trace = "$trace.summary", + ) + assertEquals(data.brackets, obj.bracket, "$trace.bracket $doesntMatchText") + } else { + assertNull(obj, "$trace should be null") + } + } + + private fun verifyInputTradeStateDeprecated(data: Map?, obj: TradeInput?, trace: String) { if (data != null) { assertNotNull(obj, "$trace should not be null") assertEquals( @@ -410,12 +493,12 @@ open class BaseTests( obj.marketOrder, "$trace.marketOrder", ) - verifyInputTradeInputOptionsState( + verifyInputTradeInputOptionsStateDeprecated( parser.asNativeMap(data["options"]), obj.options, "$trace.options", ) - verifyInputTradeInputSummaryState( + verifyInputTradeInputSummaryStateDeprecated( parser.asNativeMap(data["summary"]), obj.summary, "$trace.summary", @@ -452,7 +535,7 @@ open class BaseTests( obj.marketOrder, "$trace.marketOrder", ) - verifyInputTradeInputSummaryState( + verifyInputTradeInputSummaryStateDeprecated( parser.asNativeMap(data["summary"]), obj.summary, "$trace.summary", @@ -638,6 +721,32 @@ open class BaseTests( } private fun verifyInputTradeInputOptionsState( + data: InternalTradeInputOptions?, + obj: TradeInputOptions?, + trace: String, + ) { + if (data != null) { + assertNotNull(obj) + assertEquals(data.needsMarginMode, obj.needsMarginMode, "$trace.needsMarginMode $doesntMatchText") + assertEquals(data.needsSize, obj.needsSize, "$trace.needsSize $doesntMatchText") + assertEquals(data.needsLeverage, obj.needsLeverage, "$trace.needsLeverage $doesntMatchText") + assertEquals(data.needsBrackets, obj.needsBrackets, "$trace.needsBrackets $doesntMatchText") + assertEquals(data.needsGoodUntil, obj.needsGoodUntil, "$trace.needsGoodUntil $doesntMatchText") + assertEquals(data.needsLimitPrice, obj.needsLimitPrice, "$trace.needsLimitPrice $doesntMatchText") + assertEquals(data.needsPostOnly, obj.needsPostOnly, "$trace.needsPostOnly $doesntMatchText") + assertEquals(data.needsReduceOnly, obj.needsReduceOnly, "$trace.needsReduceOnly $doesntMatchText") + assertEquals(data.needsTrailingPercent, obj.needsTrailingPercent, "$trace.needsTrailingPercent $doesntMatchText") + assertEquals(data.needsTriggerPrice, obj.needsTriggerPrice, "$trace.needsTriggerPrice $doesntMatchText") + assertEquals(data.executionOptions, obj.executionOptions, "$trace.executionOptions $doesntMatchText") + assertEquals(data.timeInForceOptions, obj.timeInForceOptions, "$trace.timeInForceOptions $doesntMatchText") + assertEquals(data.reduceOnlyTooltip, obj.reduceOnlyTooltip, "$trace.timeInForceOptions $doesntMatchText") + assertEquals(data.postOnlyTooltip, obj.postOnlyTooltip, "$trace.timeInForceOptions $doesntMatchText") + } else { + assertNull(obj) + } + } + + private fun verifyInputTradeInputOptionsStateDeprecated( data: Map?, obj: TradeInputOptions?, trace: String, @@ -774,6 +883,25 @@ open class BaseTests( } private fun verifyInputTradeInputSummaryState( + data: InternalTradeInputSummary?, + obj: TradeInputSummary?, + trace: String, + ) { + if (data != null) { + assertNotNull(obj) + assertEquals(data.filled, obj.filled, "$trace.filled $doesntMatchText") + assertEquals(data.size, obj.size, "$trace.size $doesntMatchText") + assertEquals(data.price, obj.price, "$trace.price $doesntMatchText") + assertEquals(data.fee, obj.fee, "$trace.fee $doesntMatchText") + assertEquals(data.slippage, obj.slippage, "$trace.slippage $doesntMatchText") + assertEquals(data.usdcSize, obj.usdcSize, "$trace.usdcSize $doesntMatchText") + assertEquals(data.total, obj.total, "$trace.total $doesntMatchText") + } else { + assertNull(obj) + } + } + + private fun verifyInputTradeInputSummaryStateDeprecated( data: Map?, obj: TradeInputSummary?, trace: String, @@ -1001,7 +1129,7 @@ open class BaseTests( if (internalState != null) { assertNotNull(obj) assertEquals(internalState.subaccountNumber, obj.subaccountNumber, "$trace.subaccountNumber") - assertEquals(internalState.marginEnabled, obj.marginEnabled, "$trace.marginEnabled") + assertEquals(internalState.marginEnabled ?: true, obj.marginEnabled ?: true, "$trace.marginEnabled") // TODO: Calculated fields // assertEquals(parser.asDouble(data["pnl24h"]), obj.pnl24h, "$trace.pnl24h") // assertEquals( @@ -1011,6 +1139,57 @@ open class BaseTests( // ) // assertEquals(parser.asDouble(data["pnlTotal"]), obj.pnlTotal, "$trace.pnlTotal") // assertEquals(parser.asString(data["positionId"]), obj.positionId, "$trace.positionId + + assertEquals(internalState.calculated[CalculationPeriod.current]?.equity, obj.equity?.current, "$trace.equity") + assertEquals(internalState.calculated[CalculationPeriod.post]?.equity, obj.equity?.postOrder, "$trace.equity") + assertEquals(internalState.calculated[CalculationPeriod.settled]?.equity, obj.equity?.postAllOrders, "$trace.equity") + + assertEquals(internalState.calculated[CalculationPeriod.current]?.leverage, obj.leverage?.current, "$trace.leverage") + assertEquals(internalState.calculated[CalculationPeriod.post]?.leverage, obj.leverage?.postOrder, "$trace.leverage") + assertEquals(internalState.calculated[CalculationPeriod.settled]?.leverage, obj.leverage?.postAllOrders, "$trace.leverage") + + assertEquals(internalState.calculated[CalculationPeriod.current]?.quoteBalance, obj.quoteBalance?.current, "$trace.quoteBalance") + assertEquals(internalState.calculated[CalculationPeriod.post]?.quoteBalance, obj.quoteBalance?.postOrder, "$trace.quoteBalance") + assertEquals(internalState.calculated[CalculationPeriod.settled]?.quoteBalance, obj.quoteBalance?.postAllOrders, "$trace.quoteBalance") + + assertEquals(internalState.calculated[CalculationPeriod.current]?.initialRiskTotal, obj.initialRiskTotal?.current, "$trace.initialRiskTotal") + assertEquals(internalState.calculated[CalculationPeriod.post]?.initialRiskTotal, obj.initialRiskTotal?.postOrder, "$trace.initialRiskTotal") + assertEquals(internalState.calculated[CalculationPeriod.settled]?.initialRiskTotal, obj.initialRiskTotal?.postAllOrders, "$trace.initialRiskTotal") + + assertEquals(internalState.calculated[CalculationPeriod.current]?.quoteBalance, obj.quoteBalance?.current, "$trace.quoteBalance") + assertEquals(internalState.calculated[CalculationPeriod.post]?.quoteBalance, obj.quoteBalance?.postOrder, "$trace.quoteBalance") + assertEquals(internalState.calculated[CalculationPeriod.settled]?.quoteBalance, obj.quoteBalance?.postAllOrders, "$trace.quoteBalance") + + assertEquals(internalState.calculated[CalculationPeriod.current]?.valueTotal, obj.valueTotal?.current, "$trace.valueTotal") + assertEquals(internalState.calculated[CalculationPeriod.post]?.valueTotal, obj.valueTotal?.postOrder, "$trace.valueTotal") + assertEquals(internalState.calculated[CalculationPeriod.settled]?.valueTotal, obj.valueTotal?.postAllOrders, "$trace.valueTotal") + + assertEquals(internalState.calculated[CalculationPeriod.current]?.buyingPower, obj.buyingPower?.current, "$trace.buyingPower") + assertEquals(internalState.calculated[CalculationPeriod.post]?.buyingPower, obj.buyingPower?.postOrder, "$trace.buyingPower") + assertEquals(internalState.calculated[CalculationPeriod.settled]?.buyingPower, obj.buyingPower?.postAllOrders, "$trace.buyingPower") + + assertEquals(internalState.calculated[CalculationPeriod.current]?.freeCollateral, obj.freeCollateral?.current, "$trace.freeCollateral") + assertEquals(internalState.calculated[CalculationPeriod.post]?.freeCollateral, obj.freeCollateral?.postOrder, "$trace.freeCollateral") + assertEquals(internalState.calculated[CalculationPeriod.settled]?.freeCollateral, obj.freeCollateral?.postAllOrders, "$trace.freeCollateral") + + assertEquals(internalState.calculated[CalculationPeriod.current]?.marginUsage, obj.marginUsage?.current, "$trace.marginUsage") + assertEquals(internalState.calculated[CalculationPeriod.post]?.marginUsage, obj.marginUsage?.postOrder, "$trace.marginUsage") + assertEquals(internalState.calculated[CalculationPeriod.settled]?.marginUsage, obj.marginUsage?.postAllOrders, "$trace.marginUsage") + + assertEquals(internalState.calculated[CalculationPeriod.current]?.notionalTotal, obj.notionalTotal?.current, "$trace.notionalTotal") + assertEquals(internalState.calculated[CalculationPeriod.post]?.notionalTotal, obj.notionalTotal?.postOrder, "$trace.notionalTotal") + assertEquals(internalState.calculated[CalculationPeriod.settled]?.notionalTotal, obj.notionalTotal?.postAllOrders, "$trace.notionalTotal") + + verifyAccountSubaccountOpenPositions( + data = internalState.openPositions, + obj = obj.openPositions, + trace = "$trace.openPositions", + ) +// verifyAccountSubaccountPendingPositions( +// parser.asNativeList(data["pendingPositions"]), +// obj.pendingPositions, +// "$trace.pendingPositions", +// ) } else { assertNull(obj) } @@ -1087,7 +1266,7 @@ open class BaseTests( obj.quoteBalance, "$trace.quoteBalance", ) - verifyAccountSubaccountOpenPositions( + verifyAccountSubaccountOpenPositionsDeprecated( parser.asNativeMap(data["openPositions"]), obj.openPositions, "$trace.openPositions", @@ -1117,6 +1296,28 @@ open class BaseTests( } private fun verifyAccountSubaccountOpenPositions( + data: Map?, + obj: List?, + trace: String, + ) { + if (data != null) { + val obj = obj ?: emptyList() + assertEquals(data.size, obj.size, "$trace.size $doesntMatchText") + for (position in obj) { + val positionId = position.id + verifyAccountSubaccountOpenPosition( + data = data[positionId], + key = positionId, + obj = position, + trace = "$trace.$positionId", + ) + } + } else { + assertNull(obj) + } + } + + private fun verifyAccountSubaccountOpenPositionsDeprecated( data: Map?, obj: List?, trace: String, @@ -1126,7 +1327,7 @@ open class BaseTests( assertEquals(data.size, obj.size, "$trace.size $doesntMatchText") for (position in obj) { val positionId = position.id - verifyAccountSubaccountOpenPosition( + verifyAccountSubaccountOpenPositionDeprecated( parser.asNativeMap(data[positionId]), position, "$trace.$positionId", @@ -1138,6 +1339,25 @@ open class BaseTests( } private fun verifyAccountSubaccountOpenPosition( + data: InternalPerpetualPosition?, + key: String, + obj: SubaccountPosition?, + trace: String, + ) { + if (data != null) { + assertNotNull(obj) + assertEquals(key, obj.id, "$trace.id") + assertEquals(MarketId.getAssetId(key), obj.assetId, "$trace.assetId") + assertEquals(MarketId.getDisplayId(key), obj.displayId, "$trace.displayId") + assertEquals(data.exitPrice, obj.exitPrice, "$trace.exitPrice") + assertEquals(data.netFunding, obj.netFunding, "$trace.netFunding") + assertEquals(data.closedAt?.toEpochMilliseconds()?.toDouble(), obj.closedAtMilliseconds, "$trace.closedAt") + } else { + assertNull(obj) + } + } + + private fun verifyAccountSubaccountOpenPositionDeprecated( data: Map?, obj: SubaccountPosition?, trace: String, diff --git a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v3/V3TradeInputWithoutAccountTests.kt b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v3/V3TradeInputWithoutAccountTests.kt index 7b91e0dec..e333f06f2 100644 --- a/src/commonTest/kotlin/exchange.dydx.abacus/payload/v3/V3TradeInputWithoutAccountTests.kt +++ b/src/commonTest/kotlin/exchange.dydx.abacus/payload/v3/V3TradeInputWithoutAccountTests.kt @@ -1,5 +1,7 @@ package exchange.dydx.abacus.payload.v3 +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 @@ -7,6 +9,7 @@ import exchange.dydx.abacus.tests.extensions.loadAccounts import exchange.dydx.abacus.tests.extensions.log import exchange.dydx.abacus.utils.ServerTime import kotlin.test.Test +import kotlin.test.assertEquals class V3TradeInputWithoutAccountTests : V3BaseTests() { @Test @@ -44,11 +47,18 @@ class V3TradeInputWithoutAccountTests : V3BaseTests() { perp.trade("MARKET", TradeInputField.type, 0) }, null) - test( - { - perp.trade("1.", TradeInputField.size, 0) - }, - """ + if (perp.staticTyping) { + perp.trade("1.", TradeInputField.size, 0) + val trade = perp.internalState.input.trade + assertEquals(trade.type, OrderType.Market) + assertEquals(trade.side, OrderSide.Buy) + assertEquals(trade.marketId, "ETH-USD") + } else { + test( + { + perp.trade("1.", TradeInputField.size, 0) + }, + """ { "input": { "trade": { @@ -74,16 +84,22 @@ class V3TradeInputWithoutAccountTests : V3BaseTests() { "current": "trade" } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } private fun testLoadAccounts() { - test( - { - perp.loadAccounts(mock) - }, - """ + if (perp.staticTyping) { + perp.loadAccounts(mock) + val account = perp.internalState.wallet.account + assertEquals(account.subaccounts.size, 1) + } else { + test( + { + perp.loadAccounts(mock) + }, + """ { "wallet": { "account": { @@ -117,7 +133,8 @@ class V3TradeInputWithoutAccountTests : V3BaseTests() { "current": "trade" } } - """.trimIndent(), - ) + """.trimIndent(), + ) + } } } From 7f65928b824dd67c35b75e7bdb3d6cad16aa6ced Mon Sep 17 00:00:00 2001 From: mobile-build-bot-git Date: Thu, 22 Aug 2024 05:38:28 +0000 Subject: [PATCH 55/63] Bump version --- build.gradle.kts | 2 +- v4_abacus.podspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index e8dbefbdf..b00da5989 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -52,7 +52,7 @@ allprojects { } group = "exchange.dydx.abacus" -version = "1.8.96" +version = "1.8.97" repositories { google() diff --git a/v4_abacus.podspec b/v4_abacus.podspec index d1efedc7b..b0f41a24e 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.96' + spec.version = '1.8.97' spec.homepage = 'https://github.com/dydxprotocol/v4-abacus' spec.source = { :http=> ''} spec.authors = '' From 38f2b33e1da9c8a84c613eb91f553da5a2416902 Mon Sep 17 00:00:00 2001 From: mobile-build-bot-git Date: Thu, 22 Aug 2024 05:58:58 +0000 Subject: [PATCH 56/63] Bump version --- build.gradle.kts | 2 +- v4_abacus.podspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index b00da5989..8427d025a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -52,7 +52,7 @@ allprojects { } group = "exchange.dydx.abacus" -version = "1.8.97" +version = "1.8.98" repositories { google() diff --git a/v4_abacus.podspec b/v4_abacus.podspec index b0f41a24e..edfd9d764 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.97' + spec.version = '1.8.98' spec.homepage = 'https://github.com/dydxprotocol/v4-abacus' spec.source = { :http=> ''} spec.authors = '' From 1b425a2f9b61ad2bd7342f95235f8e15bc972665 Mon Sep 17 00:00:00 2001 From: Rui Date: Thu, 22 Aug 2024 15:52:59 +0800 Subject: [PATCH 57/63] ClosePositionInput --- .../calculator/MarginCalculator.kt | 14 +- .../input/ClosePositionInputProcessor.kt | 198 ++++++++++++++++++ .../processor/input/TradeInputProcessor.kt | 10 +- .../state/internalstate/InternalState.kt | 1 + .../TradingStateMachine+ClosePositionInput.kt | 167 ++++++++------- .../model/TradingStateMachine+TradeInput.kt | 2 + .../state/model/TradingStateMachine.kt | 2 + 7 files changed, 319 insertions(+), 75 deletions(-) create mode 100644 src/commonMain/kotlin/exchange.dydx.abacus/processor/input/ClosePositionInputProcessor.kt diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginCalculator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginCalculator.kt index 5b3a43106..c12df5874 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginCalculator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/MarginCalculator.kt @@ -26,11 +26,11 @@ import kotlin.math.min internal object MarginCalculator { fun findExistingPosition( - account: InternalAccountState, + account: InternalAccountState?, marketId: String?, subaccountNumber: Int, ): InternalPerpetualPosition? { - val position = account.groupedSubaccounts[subaccountNumber]?.openPositions?.get(marketId) + val position = account?.groupedSubaccounts?.get(subaccountNumber)?.openPositions?.get(marketId) return if ( (position?.size ?: 0.0) != 0.0 ) { @@ -420,6 +420,16 @@ internal object MarginCalculator { } fun getChildSubaccountNumberForIsolatedMarginClosePosition( + account: InternalAccountState?, + subaccountNumber: Int, + tradeInput: InternalTradeInputState? + ): Int { + val marketId = tradeInput?.marketId ?: return subaccountNumber + val position = findExistingPosition(account, marketId, subaccountNumber) + return position?.subaccountNumber ?: subaccountNumber + } + + fun getChildSubaccountNumberForIsolatedMarginClosePositionDeprecated( parser: ParserProtocol, account: Map?, subaccountNumber: Int, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/ClosePositionInputProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/ClosePositionInputProcessor.kt new file mode 100644 index 000000000..8926047f4 --- /dev/null +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/ClosePositionInputProcessor.kt @@ -0,0 +1,198 @@ +package exchange.dydx.abacus.processor.input + +import abs +import exchange.dydx.abacus.calculator.CalculationPeriod +import exchange.dydx.abacus.calculator.MarginCalculator +import exchange.dydx.abacus.calculator.TradeCalculation +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.TradeInputSize +import exchange.dydx.abacus.protocols.ParserProtocol +import exchange.dydx.abacus.responses.ParsingError +import exchange.dydx.abacus.responses.cannotModify +import exchange.dydx.abacus.state.changes.Changes +import exchange.dydx.abacus.state.changes.StateChanges +import exchange.dydx.abacus.state.internalstate.InternalConfigsState +import exchange.dydx.abacus.state.internalstate.InternalInputState +import exchange.dydx.abacus.state.internalstate.InternalMarketSummaryState +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.model.ClosePositionInputField +import exchange.dydx.abacus.utils.Numeric +import kollections.iListOf + +internal interface ClosePositionInputProcessorProtocol + +internal class ClosePositionInputProcessor( + private val parser: ParserProtocol, +) : ClosePositionInputProcessorProtocol { + + fun closePosition( + inputState: InternalInputState, + walletState: InternalWalletState, + marketSummaryState: InternalMarketSummaryState, + configs: InternalConfigsState, + rewardsParams: InternalRewardsParamsState?, + data: String?, + type: ClosePositionInputField, + subaccountNumber: Int, + ): TradeInputResult { + var changes: StateChanges? = null + var error: ParsingError? = null + + if (inputState.currentType != InputType.CLOSE_POSITION) { + inputState.currentType = InputType.CLOSE_POSITION + inputState.closePosition = initiateClosePosition( + marketId = null, + subaccountNumber = subaccountNumber, + walletState = walletState, + marketSummaryState = marketSummaryState, + configs = configs, + rewardsParams = rewardsParams, + ) + } + inputState.currentType = InputType.CLOSE_POSITION + + val childSubaccountNumber = + MarginCalculator.getChildSubaccountNumberForIsolatedMarginClosePosition( + account = walletState.account, + subaccountNumber = subaccountNumber, + tradeInput = inputState.closePosition, + ) + val subaccountNumberChanges = if (subaccountNumber == childSubaccountNumber) { + iListOf(subaccountNumber) + } else { + iListOf(subaccountNumber, childSubaccountNumber) + } + + var sizeChanged = false + val trade = inputState.closePosition + when (type) { + ClosePositionInputField.market -> { + val position = if (data != null) getPosition(data, subaccountNumber, walletState) else null + if (position != null) { + if (data != null) { + if (trade.marketId != data) { + trade.marketId = data + trade.size = null + } + } + trade.type = OrderType.Market + + val positionSize = position.calculated[CalculationPeriod.current]?.size ?: Numeric.double.ZERO + trade.side = if (positionSize > Numeric.double.ZERO) OrderSide.Sell else OrderSide.Buy + + trade.timeInForce = "IOC" + trade.reduceOnly = true + + val currentPositionLeverage = position.calculated[CalculationPeriod.current]?.leverage?.abs() + trade.targetLeverage = if (currentPositionLeverage != null && currentPositionLeverage > 0) currentPositionLeverage else 1.0 + + // default full close + trade.sizePercent = 1.0 + trade.size = TradeInputSize( + size = null, + usdcSize = null, + leverage = null, + input = "size.percent", + ) + + changes = StateChanges( + changes = iListOf(Changes.subaccount, Changes.input), + markets = null, + subaccountNumbers = subaccountNumberChanges, + ) + } else { + error = ParsingError.cannotModify(type.rawValue) + } + } + ClosePositionInputField.size -> { + val newSize = parser.asDouble(data) + sizeChanged = (newSize != trade.size?.size) + trade.size = trade.size?.copy(size = newSize) + changes = StateChanges( + changes = iListOf(Changes.subaccount, Changes.input), + markets = null, + subaccountNumbers = subaccountNumberChanges, + ) + } + ClosePositionInputField.percent -> { + val newPercent = parser.asDouble(data) + sizeChanged = (newPercent != trade.sizePercent) + trade.sizePercent = newPercent + changes = StateChanges( + changes = iListOf(Changes.subaccount, Changes.input), + markets = null, + subaccountNumbers = subaccountNumberChanges, + ) + } + } + if (sizeChanged) { + when (type) { + ClosePositionInputField.size, + ClosePositionInputField.percent -> { + trade.size = trade.size?.copy(input = type.rawValue) + } + else -> { } + } + } + + return TradeInputResult( + changes = changes, + error = error, + ) + } + + private fun getPosition( + marketId: String, + subaccountNumber: Int, + wallet: InternalWalletState, + ): InternalPerpetualPosition? { + val position = wallet.account.groupedSubaccounts[subaccountNumber]?.openPositions?.get(marketId) + ?: wallet.account.subaccounts[subaccountNumber]?.openPositions?.get(marketId) + + val size = position?.calculated?.get(CalculationPeriod.current)?.size + return if (size != null && size != Numeric.double.ZERO) { + position + } else { + null + } + } + + private fun initiateClosePosition( + marketId: String?, + subaccountNumber: Int, + walletState: InternalWalletState, + marketSummaryState: InternalMarketSummaryState, + configs: InternalConfigsState, + rewardsParams: InternalRewardsParamsState? + ): InternalTradeInputState { + val closePosition = InternalTradeInputState() + closePosition.type = OrderType.Market + closePosition.side = OrderSide.Buy + closePosition.marketId = marketId ?: "ETH-USD" + // default full close + closePosition.sizePercent = 1.0 + closePosition.size = TradeInputSize( + size = null, + usdcSize = null, + leverage = null, + input = "size.percent", + ) + + val calculator = TradeInputCalculatorV2(parser, TradeCalculation.closePosition) + return calculator.calculate( + trade = closePosition, + wallet = walletState, + marketSummary = marketSummaryState, + rewardsParams = rewardsParams, + configs = configs, + subaccountNumber = subaccountNumber, + input = "size.percent", + ) + } +} diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputProcessor.kt index 973f7a2f0..a2c59cd47 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TradeInputProcessor.kt @@ -22,6 +22,7 @@ import exchange.dydx.abacus.state.internalstate.InternalConfigsState import exchange.dydx.abacus.state.internalstate.InternalInputState import exchange.dydx.abacus.state.internalstate.InternalMarketState import exchange.dydx.abacus.state.internalstate.InternalMarketSummaryState +import exchange.dydx.abacus.state.internalstate.InternalRewardsParamsState import exchange.dydx.abacus.state.internalstate.InternalTradeInputOptions import exchange.dydx.abacus.state.internalstate.InternalTradeInputState import exchange.dydx.abacus.state.internalstate.InternalWalletState @@ -35,6 +36,7 @@ internal interface TradeInputProcessorProtocol { marketSummaryState: InternalMarketSummaryState, walletState: InternalWalletState, configs: InternalConfigsState, + rewardsParams: InternalRewardsParamsState?, marketId: String, subaccountNumber: Int, ): StateChanges @@ -44,6 +46,7 @@ internal interface TradeInputProcessorProtocol { walletState: InternalWalletState, marketSummaryState: InternalMarketSummaryState, configs: InternalConfigsState, + rewardsParams: InternalRewardsParamsState?, inputData: String?, inputType: TradeInputField?, subaccountNumber: Int, @@ -64,6 +67,7 @@ internal class TradeInputProcessor( marketSummaryState: InternalMarketSummaryState, walletState: InternalWalletState, configs: InternalConfigsState, + rewardsParams: InternalRewardsParamsState?, marketId: String, subaccountNumber: Int, ): StateChanges { @@ -94,6 +98,7 @@ internal class TradeInputProcessor( walletState = walletState, marketSummaryState = marketSummaryState, configs = configs, + rewardsParams = rewardsParams, ) } @@ -127,6 +132,7 @@ internal class TradeInputProcessor( walletState: InternalWalletState, marketSummaryState: InternalMarketSummaryState, configs: InternalConfigsState, + rewardsParams: InternalRewardsParamsState?, inputData: String?, inputType: TradeInputField?, subaccountNumber: Int, @@ -141,6 +147,7 @@ internal class TradeInputProcessor( walletState = walletState, marketSummaryState = marketSummaryState, configs = configs, + rewardsParams = rewardsParams, ) } if (inputType == null) { @@ -316,6 +323,7 @@ internal class TradeInputProcessor( walletState: InternalWalletState, marketSummaryState: InternalMarketSummaryState, configs: InternalConfigsState, + rewardsParams: InternalRewardsParamsState?, ): InternalTradeInputState { val market = marketSummaryState.markets[marketId] val marginMode = MarginCalculator.findExistingMarginMode( @@ -337,7 +345,7 @@ internal class TradeInputProcessor( ), wallet = walletState, marketSummary = marketSummaryState, - rewardsParams = null, + rewardsParams = rewardsParams, configs = configs, subaccountNumber = subaccountNumber, input = null, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt index d522f7e3f..86e78812f 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/internalstate/InternalState.kt @@ -55,6 +55,7 @@ internal data class InternalState( internal data class InternalInputState( var trade: InternalTradeInputState = InternalTradeInputState(), + var closePosition: InternalTradeInputState = InternalTradeInputState(), var receiptLines: List? = null, var errors: List? = null, var childSubaccountErrors: List? = null, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+ClosePositionInput.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+ClosePositionInput.kt index d1d185c68..0845216fd 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+ClosePositionInput.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+ClosePositionInput.kt @@ -35,97 +35,120 @@ fun TradingStateMachine.closePosition( type: ClosePositionInputField, subaccountNumber: Int ): StateResponse { - var changes: StateChanges? = null - var error: ParsingError? = null - val typeText = type.rawValue - - val input = this.input?.mutable() ?: mutableMapOf() - input["current"] = "closePosition" - val trade = - parser.asMap(input["closePosition"])?.mutable() ?: initiateClosePosition( - null, - subaccountNumber, + if (staticTyping) { + val result = closePositionInputProcessor.closePosition( + inputState = internalState.input, + walletState = internalState.wallet, + marketSummaryState = internalState.marketsSummary, + configs = internalState.configs, + rewardsParams = internalState.rewardsParams, + data = data, + type = type, + subaccountNumber = subaccountNumber, ) - - val childSubaccountNumber = - MarginCalculator.getChildSubaccountNumberForIsolatedMarginClosePosition( - parser, - account, - subaccountNumber, - trade, - ) - val subaccountNumberChanges = if (subaccountNumber == childSubaccountNumber) { - iListOf(subaccountNumber) + result.changes?.let { + updateStateChanges(it) + } + return StateResponse(state, result.changes, if (result.error != null) iListOf(result.error) else null) } else { - iListOf(subaccountNumber, childSubaccountNumber) - } + var changes: StateChanges? = null + var error: ParsingError? = null + val typeText = type.rawValue + + val input = this.input?.mutable() ?: mutableMapOf() + input["current"] = "closePosition" + val trade = + parser.asMap(input["closePosition"])?.mutable() ?: initiateClosePosition( + null, + subaccountNumber, + ) + + val childSubaccountNumber = + MarginCalculator.getChildSubaccountNumberForIsolatedMarginClosePositionDeprecated( + parser, + account, + subaccountNumber, + trade, + ) + val subaccountNumberChanges = if (subaccountNumber == childSubaccountNumber) { + iListOf(subaccountNumber) + } else { + iListOf(subaccountNumber, childSubaccountNumber) + } - var sizeChanged = false - when (typeText) { - ClosePositionInputField.market.rawValue -> { - val position = if (data != null) getPosition(data, subaccountNumber) else null - if (position != null) { - if (data != null) { - if (parser.asString(trade["marketId"]) != data) { - trade.safeSet("marketId", data) - trade.safeSet("size", null) + var sizeChanged = false + when (typeText) { + ClosePositionInputField.market.rawValue -> { + val position = if (data != null) getPosition(data, subaccountNumber) else null + if (position != null) { + if (data != null) { + if (parser.asString(trade["marketId"]) != data) { + trade.safeSet("marketId", data) + trade.safeSet("size", null) + } } + trade["type"] = "MARKET" + + val positionSize = + parser.asDouble(parser.value(position, "size.current")) + ?: Numeric.double.ZERO + trade["side"] = if (positionSize > Numeric.double.ZERO) "SELL" else "BUY" + + trade["timeInForce"] = "IOC" + trade["reduceOnly"] = true + + val currentPositionLeverage = + parser.asDouble(parser.value(position, "leverage.current"))?.abs() + trade["targetLeverage"] = + if (currentPositionLeverage != null && currentPositionLeverage > 0) currentPositionLeverage else 1.0 + + // default full close + trade.safeSet("size.percent", 1.0) + trade.safeSet("size.input", "size.percent") + + changes = StateChanges( + iListOf(Changes.subaccount, Changes.input), + null, + subaccountNumberChanges, + ) + } else { + error = ParsingError.cannotModify(typeText) } - trade["type"] = "MARKET" - - val positionSize = - parser.asDouble(parser.value(position, "size.current")) ?: Numeric.double.ZERO - trade["side"] = if (positionSize > Numeric.double.ZERO) "SELL" else "BUY" - - trade["timeInForce"] = "IOC" - trade["reduceOnly"] = true - - val currentPositionLeverage = parser.asDouble(parser.value(position, "leverage.current"))?.abs() - trade["targetLeverage"] = if (currentPositionLeverage != null && currentPositionLeverage > 0) currentPositionLeverage else 1.0 - - // default full close - trade.safeSet("size.percent", 1.0) - trade.safeSet("size.input", "size.percent") + } + ClosePositionInputField.size.rawValue, ClosePositionInputField.percent.rawValue -> { + sizeChanged = (parser.asDouble(data) != parser.asDouble(trade[typeText])) + trade.safeSet(typeText, data) changes = StateChanges( iListOf(Changes.subaccount, Changes.input), null, subaccountNumberChanges, ) - } else { - error = ParsingError.cannotModify(typeText) } + + else -> {} } - ClosePositionInputField.size.rawValue, ClosePositionInputField.percent.rawValue -> { - sizeChanged = (parser.asDouble(data) != parser.asDouble(trade[typeText])) - trade.safeSet(typeText, data) - changes = StateChanges( - iListOf(Changes.subaccount, Changes.input), - null, - subaccountNumberChanges, - ) - } - else -> {} - } - if (sizeChanged) { - when (typeText) { - ClosePositionInputField.size.rawValue, - ClosePositionInputField.percent.rawValue -> { - trade.safeSet("size.input", typeText) + if (sizeChanged) { + when (typeText) { + ClosePositionInputField.size.rawValue, + ClosePositionInputField.percent.rawValue -> { + trade.safeSet("size.input", typeText) + } + + else -> {} } - else -> {} } - } - input["closePosition"] = trade - this.input = input + input["closePosition"] = trade + this.input = input - changes?.let { - updateStateChanges(it) + changes?.let { + updateStateChanges(it) + } + return StateResponse(state, changes, if (error != null) iListOf(error) else null) } - return StateResponse(state, changes, if (error != null) iListOf(error) else null) } -fun TradingStateMachine.getPosition( +private fun TradingStateMachine.getPosition( marketId: String, subaccountNumber: Int, ): Map? { diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt index 7fa839c8b..fba530bdb 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt @@ -95,6 +95,7 @@ internal fun TradingStateMachine.tradeInMarket( marketSummaryState = internalState.marketsSummary, walletState = internalState.wallet, configs = internalState.configs, + rewardsParams = internalState.rewardsParams, marketId = marketId, subaccountNumber = subaccountNumber, ) @@ -228,6 +229,7 @@ fun TradingStateMachine.trade( walletState = internalState.wallet, marketSummaryState = internalState.marketsSummary, configs = internalState.configs, + rewardsParams = internalState.rewardsParams, inputType = type, inputData = data, subaccountNumber = subaccountNumber, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt index d296ca4c5..1f737b9d0 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt @@ -37,6 +37,7 @@ import exchange.dydx.abacus.output.input.ReceiptLine import exchange.dydx.abacus.processor.assets.AssetsProcessor import exchange.dydx.abacus.processor.configs.ConfigsProcessor import exchange.dydx.abacus.processor.configs.RewardsParamsProcessor +import exchange.dydx.abacus.processor.input.ClosePositionInputProcessor import exchange.dydx.abacus.processor.input.TradeInputProcessor import exchange.dydx.abacus.processor.launchIncentive.LaunchIncentiveProcessor import exchange.dydx.abacus.processor.markets.MarketsSummaryProcessor @@ -127,6 +128,7 @@ open class TradingStateMachine( internal val rewardsProcessor = RewardsParamsProcessor(parser) internal val launchIncentiveProcessor = LaunchIncentiveProcessor(parser) internal val tradeInputProcessor = TradeInputProcessor(parser) + internal val closePositionInputProcessor = ClosePositionInputProcessor(parser) internal val marketsCalculator = MarketCalculator(parser) internal val accountCalculator = AccountCalculator(parser, useParentSubaccount) From 36b34f26e07763b502438f16d2facd0acff945f5 Mon Sep 17 00:00:00 2001 From: Rui Date: Thu, 22 Aug 2024 19:28:08 +0800 Subject: [PATCH 58/63] Validation --- .../output/input/ClosePositionInput.kt | 25 +++++++++++++++++++ .../output/input/Input.kt | 6 ++++- .../state/model/TradingStateMachine.kt | 25 ++++++++++++------- .../trade/TradeAccountStateValidator.kt | 7 +++++- .../trade/TradeBracketOrdersValidator.kt | 7 +++++- .../validator/trade/TradeFieldsValidator.kt | 7 +++++- .../trade/TradeInputDataValidator.kt | 8 +++++- .../trade/TradeMarketOrderInputValidator.kt | 7 +++++- .../trade/TradePositionStateValidator.kt | 7 +++++- .../trade/TradeResctrictedValidator.kt | 8 +++++- .../trade/TradeTriggerPriceValidator.kt | 7 +++++- 11 files changed, 96 insertions(+), 18 deletions(-) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/output/input/ClosePositionInput.kt b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/ClosePositionInput.kt index 021f739cc..04d6664b1 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/output/input/ClosePositionInput.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/ClosePositionInput.kt @@ -1,6 +1,7 @@ package exchange.dydx.abacus.output.input import exchange.dydx.abacus.protocols.ParserProtocol +import exchange.dydx.abacus.state.internalstate.InternalTradeInputState import exchange.dydx.abacus.utils.Logger import kollections.JsExport import kotlinx.serialization.Serializable @@ -55,6 +56,30 @@ data class ClosePositionInput( val summary: TradeInputSummary? ) { companion object { + internal fun create( + state: InternalTradeInputState? + ): ClosePositionInput? { + if (state == null) { + return null + } + + return ClosePositionInput( + type = state.type, + side = state.side, + marketId = state.marketId, + size = ClosePositionInputSize( + size = state.size?.size, + usdcSize = state.size?.usdcSize, + percent = state.sizePercent, + input = state.size?.input, + ), + price = state.price, + fee = state.fee, + marketOrder = state.marketOrder, + summary = TradeInputSummary.create(state.summary), + ) + } + internal fun create( existing: ClosePositionInput?, parser: ParserProtocol, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/output/input/Input.kt b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/Input.kt index aae4f1e6a..92fcb8d52 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/output/input/Input.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/Input.kt @@ -63,8 +63,12 @@ data class Input( } else { TradeInput.create(existing?.trade, parser, parser.asMap(data?.get("trade"))) } - val closePosition = + + val closePosition = if (staticTyping) { + ClosePositionInput.create(state = internalState?.input?.closePosition) + } else { ClosePositionInput.create(existing?.closePosition, parser, parser.asMap(data?.get("closePosition"))) + } val transfer = TransferInput.create(existing?.transfer, parser, parser.asMap(data?.get("transfer")), environment, internalState?.transfer) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt index 1f737b9d0..dc2ea9bd7 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt @@ -758,15 +758,22 @@ open class TradingStateMachine( private fun calculateTrade(tag: String, calculation: TradeCalculation, subaccountNumber: Int) { if (staticTyping) { val calculator = TradeInputCalculatorV2(parser, calculation) - calculator.calculate( - trade = internalState.input.trade, - wallet = internalState.wallet, - marketSummary = internalState.marketsSummary, - rewardsParams = internalState.rewardsParams, - configs = internalState.configs, - subaccountNumber = subaccountNumber, - input = internalState.input.trade.size?.input, - ) + val inputType = + calculator.calculate( + trade = when (calculation) { + TradeCalculation.closePosition -> internalState.input.closePosition + TradeCalculation.trade -> internalState.input.trade + }, + wallet = internalState.wallet, + marketSummary = internalState.marketsSummary, + rewardsParams = internalState.rewardsParams, + configs = internalState.configs, + subaccountNumber = subaccountNumber, + input = when (calculation) { + TradeCalculation.closePosition -> internalState.input.closePosition.size?.input + TradeCalculation.trade -> internalState.input.trade.size?.input + }, + ) } else { val input = this.input?.mutable() val trade = parser.asNativeMap(input?.get(tag)) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeAccountStateValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeAccountStateValidator.kt index 9b69c56cd..9c89ef2d1 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeAccountStateValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeAccountStateValidator.kt @@ -3,6 +3,7 @@ package exchange.dydx.abacus.validator.trade import exchange.dydx.abacus.calculator.CalculationPeriod import exchange.dydx.abacus.output.account.SubaccountOrder import exchange.dydx.abacus.output.input.ErrorType +import exchange.dydx.abacus.output.input.InputType import exchange.dydx.abacus.output.input.OrderSide import exchange.dydx.abacus.output.input.OrderStatus import exchange.dydx.abacus.output.input.OrderType @@ -33,7 +34,11 @@ internal class TradeAccountStateValidator( restricted: Boolean, environment: V4Environment? ): List? { - val trade = internalState.input.trade + val trade = when (internalState.input.currentType) { + InputType.TRADE -> internalState.input.trade + InputType.CLOSE_POSITION -> internalState.input.closePosition + else -> return null + } val subaccountNumber = subaccountNumber ?: return null val subaccount = internalState.wallet.account.subaccounts[subaccountNumber] ?: return null val isIsolatedMarginTrade = subaccountNumber >= NUM_PARENT_SUBACCOUNTS 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 6a31afdbc..4b32b4d40 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeBracketOrdersValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeBracketOrdersValidator.kt @@ -2,6 +2,7 @@ package exchange.dydx.abacus.validator.trade import exchange.dydx.abacus.calculator.CalculationPeriod import exchange.dydx.abacus.output.input.ErrorType +import exchange.dydx.abacus.output.input.InputType import exchange.dydx.abacus.output.input.OrderSide import exchange.dydx.abacus.output.input.ValidationError import exchange.dydx.abacus.protocols.LocalizerProtocol @@ -28,7 +29,11 @@ internal class TradeBracketOrdersValidator( restricted: Boolean, environment: V4Environment? ): List? { - val trade = internalState.input.trade + val trade = when (internalState.input.currentType) { + InputType.TRADE -> internalState.input.trade + InputType.CLOSE_POSITION -> internalState.input.closePosition + else -> return null + } if (!trade.options.needsBrackets) { return null } 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 3fb30c091..c71f44f1e 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeFieldsValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeFieldsValidator.kt @@ -1,5 +1,6 @@ package exchange.dydx.abacus.validator.trade +import exchange.dydx.abacus.output.input.InputType import exchange.dydx.abacus.output.input.ValidationError import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol @@ -24,7 +25,11 @@ internal class TradeFieldsValidator( restricted: Boolean, environment: V4Environment? ): List? { - val trade = internalState.input.trade + val trade = when (internalState.input.currentType) { + InputType.TRADE -> internalState.input.trade + InputType.CLOSE_POSITION -> internalState.input.closePosition + else -> return null + } val errors = mutableListOf() if (trade.options.needsSize) { diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeInputDataValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeInputDataValidator.kt index 68db12cf3..0e8916b0c 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeInputDataValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeInputDataValidator.kt @@ -2,6 +2,7 @@ package exchange.dydx.abacus.validator.trade import abs import exchange.dydx.abacus.output.input.ErrorType +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.ValidationError @@ -48,7 +49,12 @@ internal class TradeInputDataValidator( ): List? { val errors = mutableListOf() - val marketId = internalState.input.trade.marketId ?: return null + val trade = when (internalState.input.currentType) { + InputType.TRADE -> internalState.input.trade + InputType.CLOSE_POSITION -> internalState.input.closePosition + else -> return null + } + val marketId = trade.marketId ?: return null val market = internalState.marketsSummary.markets[marketId] validateSize( diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeMarketOrderInputValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeMarketOrderInputValidator.kt index 216409ebe..5bc6864dd 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeMarketOrderInputValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeMarketOrderInputValidator.kt @@ -2,6 +2,7 @@ package exchange.dydx.abacus.validator.trade import abs import exchange.dydx.abacus.output.input.ErrorType +import exchange.dydx.abacus.output.input.InputType import exchange.dydx.abacus.output.input.OrderType import exchange.dydx.abacus.output.input.ValidationError import exchange.dydx.abacus.protocols.LocalizerProtocol @@ -29,7 +30,11 @@ internal class TradeMarketOrderInputValidator( restricted: Boolean, environment: V4Environment? ): List? { - val trade = internalState.input.trade + val trade = when (internalState.input.currentType) { + InputType.TRADE -> internalState.input.trade + InputType.CLOSE_POSITION -> internalState.input.closePosition + else -> return null + } if (trade.type != OrderType.Market) { return null } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradePositionStateValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradePositionStateValidator.kt index 98152042d..9027872d3 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradePositionStateValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradePositionStateValidator.kt @@ -2,6 +2,7 @@ package exchange.dydx.abacus.validator.trade import exchange.dydx.abacus.calculator.CalculationPeriod import exchange.dydx.abacus.output.input.ErrorType +import exchange.dydx.abacus.output.input.InputType import exchange.dydx.abacus.output.input.ValidationError import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol @@ -28,7 +29,11 @@ internal class TradePositionStateValidator( restricted: Boolean, environment: V4Environment? ): List? { - val trade = internalState.input.trade + val trade = when (internalState.input.currentType) { + InputType.TRADE -> internalState.input.trade + InputType.CLOSE_POSITION -> internalState.input.closePosition + else -> return null + } val subaccountNumber = subaccountNumber ?: return null val subaccount = internalState.wallet.account.subaccounts[subaccountNumber] val position = subaccount?.openPositions?.get(trade.marketId) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeResctrictedValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeResctrictedValidator.kt index c097c0cc8..4f76b9898 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeResctrictedValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeResctrictedValidator.kt @@ -1,6 +1,7 @@ package exchange.dydx.abacus.validator.trade import exchange.dydx.abacus.output.input.ErrorType +import exchange.dydx.abacus.output.input.InputType import exchange.dydx.abacus.output.input.ValidationError import exchange.dydx.abacus.protocols.LocalizerProtocol import exchange.dydx.abacus.protocols.ParserProtocol @@ -24,7 +25,12 @@ internal class TradeResctrictedValidator( restricted: Boolean, environment: V4Environment? ): List? { - val marketId = internalState.input.trade.marketId ?: return null + val trade = when (internalState.input.currentType) { + InputType.TRADE -> internalState.input.trade + InputType.CLOSE_POSITION -> internalState.input.closePosition + else -> return null + } + val marketId = trade.marketId ?: return null val market = internalState.marketsSummary.markets[marketId] val closeOnlyError = validateClosingOnly( diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeTriggerPriceValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeTriggerPriceValidator.kt index 92e323bec..8a00ff5a6 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeTriggerPriceValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeTriggerPriceValidator.kt @@ -2,6 +2,7 @@ package exchange.dydx.abacus.validator.trade import exchange.dydx.abacus.calculator.CalculationPeriod import exchange.dydx.abacus.output.input.ErrorType +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.ValidationError @@ -38,7 +39,11 @@ internal class TradeTriggerPriceValidator( restricted: Boolean, environment: V4Environment? ): List? { - val trade = internalState.input.trade + val trade = when (internalState.input.currentType) { + InputType.TRADE -> internalState.input.trade + InputType.CLOSE_POSITION -> internalState.input.closePosition + else -> return null + } val needsTriggerPrice = trade.options.needsTriggerPrice if (!needsTriggerPrice) { return null From 4e0627d7408cd12b02eb8b6d48600093a2d03e41 Mon Sep 17 00:00:00 2001 From: mobile-build-bot-git Date: Thu, 22 Aug 2024 20:17:15 +0000 Subject: [PATCH 59/63] Bump version --- build.gradle.kts | 2 +- v4_abacus.podspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 46f1a817c..41e5daeb4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -52,7 +52,7 @@ allprojects { } group = "exchange.dydx.abacus" -version = "1.8.101" +version = "1.8.102" repositories { google() diff --git a/v4_abacus.podspec b/v4_abacus.podspec index 68845b8c9..7f00e2bb8 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.101' + spec.version = '1.8.102' spec.homepage = 'https://github.com/dydxprotocol/v4-abacus' spec.source = { :http=> ''} spec.authors = '' From 89fd28318ab0dc8cecc5a7b503d25689cbbc6699 Mon Sep 17 00:00:00 2001 From: Rui Date: Fri, 23 Aug 2024 05:33:56 +0800 Subject: [PATCH 60/63] Lint --- .../calculator/V2/TradeInput/TradeInputMarketOrderCalculator.kt | 1 - 1 file changed, 1 deletion(-) 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 efa70ce8d..3d0d67e73 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 @@ -16,7 +16,6 @@ import exchange.dydx.abacus.state.internalstate.InternalSubaccountState import exchange.dydx.abacus.state.internalstate.InternalTradeInputState import exchange.dydx.abacus.state.internalstate.InternalUserState import exchange.dydx.abacus.state.internalstate.safeCreate -import exchange.dydx.abacus.state.model.ClosePositionInputField import exchange.dydx.abacus.utils.Numeric import exchange.dydx.abacus.utils.Rounder import kollections.toIList From ddbe26795b9699cbecfe3a9d287f768ba6957e88 Mon Sep 17 00:00:00 2001 From: Rui Date: Fri, 23 Aug 2024 05:38:33 +0800 Subject: [PATCH 61/63] Lint --- .../TradingStateMachine+ClosePositionInput.kt | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+ClosePositionInput.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+ClosePositionInput.kt index dc9523225..7bab0089b 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+ClosePositionInput.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+ClosePositionInput.kt @@ -98,17 +98,14 @@ fun TradingStateMachine.closePosition( trade["type"] = "MARKET" val positionSize = - parser.asDouble(parser.value(position, "size.current")) - ?: Numeric.double.ZERO + parser.asDouble(parser.value(position, "size.current")) ?: Numeric.double.ZERO trade["side"] = if (positionSize > Numeric.double.ZERO) "SELL" else "BUY" trade["timeInForce"] = "IOC" trade["reduceOnly"] = true - val currentPositionLeverage = - parser.asDouble(parser.value(position, "leverage.current"))?.abs() - trade["targetLeverage"] = - if (currentPositionLeverage != null && currentPositionLeverage > 0) currentPositionLeverage else 1.0 + val currentPositionLeverage = parser.asDouble(parser.value(position, "leverage.current"))?.abs() + trade["targetLeverage"] = if (currentPositionLeverage != null && currentPositionLeverage > 0) currentPositionLeverage else 1.0 // default full close trade.safeSet("size.percent", 1.0) @@ -123,7 +120,6 @@ fun TradingStateMachine.closePosition( error = ParsingError.cannotModify(typeText) } } - ClosePositionInputField.size.rawValue, ClosePositionInputField.percent.rawValue -> { sizeChanged = (parser.asDouble(data) != parser.asDouble(trade[typeText])) trade.safeSet(typeText, data) @@ -133,10 +129,8 @@ fun TradingStateMachine.closePosition( subaccountNumberChanges, ) } - ClosePositionInputField.useLimit.rawValue -> { - val useLimitClose = - (parser.asBool(data) ?: false) && StatsigConfig.ff_enable_limit_close + val useLimitClose = (parser.asBool(data) ?: false) && StatsigConfig.ff_enable_limit_close trade.safeSet(typeText, useLimitClose) if (useLimitClose) { @@ -156,7 +150,6 @@ fun TradingStateMachine.closePosition( subaccountNumberChanges, ) } - ClosePositionInputField.limitPrice.rawValue -> { trade.safeSet(typeText, parser.asDouble(data)) changes = StateChanges( @@ -165,7 +158,6 @@ fun TradingStateMachine.closePosition( subaccountNumberChanges, ) } - else -> {} } if (sizeChanged) { From c8ee7d8cb1790ed822b9d87b4ac2bcf6e2914737 Mon Sep 17 00:00:00 2001 From: Rui Date: Fri, 23 Aug 2024 05:47:36 +0800 Subject: [PATCH 62/63] Lint --- .../calculator/V2/TradeInput/TradeInputCalculatorV2.kt | 2 +- .../V2/TradeInput/TradeInputMarketOrderCalculator.kt | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputCalculatorV2.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputCalculatorV2.kt index e32b2711f..6c5b24fb1 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputCalculatorV2.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TradeInput/TradeInputCalculatorV2.kt @@ -28,7 +28,7 @@ internal class TradeInputCalculatorV2( private val parser: ParserProtocol, private val calculation: TradeCalculation, private val marginModeCalculator: TradeInputMarginModeCalculator = TradeInputMarginModeCalculator(), - private val marketOrderCalculator: TradeInputMarketOrderCalculator = TradeInputMarketOrderCalculator(calculation), + private val marketOrderCalculator: TradeInputMarketOrderCalculator = TradeInputMarketOrderCalculator(), private val nonMarketOrderCalculator: TradeInputNonMarketOrderCalculator = TradeInputNonMarketOrderCalculator(), private val optionsCalculator: TradeInputOptionsCalculator = TradeInputOptionsCalculator(parser), private val summaryCalculator: TradeInputSummaryCalculator = TradeInputSummaryCalculator(), 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 3d0d67e73..2d5aeaa9a 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 @@ -4,7 +4,6 @@ package exchange.dydx.abacus.calculator.v2.tradeinput import abs import exchange.dydx.abacus.calculator.CalculationPeriod -import exchange.dydx.abacus.calculator.TradeCalculation import exchange.dydx.abacus.output.input.OrderSide import exchange.dydx.abacus.output.input.OrderbookUsage import exchange.dydx.abacus.output.input.TradeInputMarketOrder @@ -20,9 +19,7 @@ import exchange.dydx.abacus.utils.Numeric import exchange.dydx.abacus.utils.Rounder import kollections.toIList -internal class TradeInputMarketOrderCalculator( - private val calculation: TradeCalculation, -) { +internal class TradeInputMarketOrderCalculator() { fun calculate( trade: InternalTradeInputState, market: InternalMarketState?, From ec52e8071537b702276f05820c380d8986168559 Mon Sep 17 00:00:00 2001 From: mobile-build-bot-git Date: Fri, 23 Aug 2024 22:31:50 +0000 Subject: [PATCH 63/63] Bump version --- build.gradle.kts | 2 +- v4_abacus.podspec | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 8d7dae536..fe5627273 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -52,7 +52,7 @@ allprojects { } group = "exchange.dydx.abacus" -version = "1.9.2" +version = "1.9.3" repositories { google() diff --git a/v4_abacus.podspec b/v4_abacus.podspec index 5cce547c2..8ebcae8ce 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.2' + spec.version = '1.9.3' spec.homepage = 'https://github.com/dydxprotocol/v4-abacus' spec.source = { :http=> ''} spec.authors = ''