diff --git a/build.gradle.kts b/build.gradle.kts index fe5627273..4fab1fbff 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -52,7 +52,7 @@ allprojects { } group = "exchange.dydx.abacus" -version = "1.9.3" +version = "1.9.4" repositories { google() 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 e2528fd3e..e834866a0 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/AccountCalculatorV2.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/AccountCalculatorV2.kt @@ -113,10 +113,8 @@ internal class AccountCalculatorV2( val modifiedOpenPositions = parentOpenPositions?.toMutableMap() ?: mutableMapOf() val childOpenPositions = childSubaccount.openPositions for ((market, childOpenPosition) in childOpenPositions ?: emptyMap()) { -// modifiedChildOpenPosition?.safeSet( -// "childSubaccountNumber", -// childSubaccountNumber, -// ) + childOpenPosition.childSubaccountNumber = childSubaccountNumber + modifiedOpenPositions[market] = childOpenPosition } parentSubaccount.openPositions = modifiedOpenPositions diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TriggerOrdersInputCalculatorV2.kt b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TriggerOrdersInputCalculatorV2.kt new file mode 100644 index 000000000..7230d516f --- /dev/null +++ b/src/commonMain/kotlin/exchange.dydx.abacus/calculator/V2/TriggerOrdersInputCalculatorV2.kt @@ -0,0 +1,342 @@ +package exchange.dydx.abacus.calculator.v2 + +import abs +import exchange.dydx.abacus.calculator.CalculationPeriod +import exchange.dydx.abacus.calculator.SlippageConstants.MAJOR_MARKETS +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.input.OrderSide +import exchange.dydx.abacus.output.input.OrderType +import exchange.dydx.abacus.output.input.TriggerOrderInputSummary +import exchange.dydx.abacus.output.input.TriggerPrice +import exchange.dydx.abacus.state.internalstate.InternalAccountState +import exchange.dydx.abacus.state.internalstate.InternalPerpetualPosition +import exchange.dydx.abacus.state.internalstate.InternalTriggerOrderState +import exchange.dydx.abacus.state.internalstate.InternalTriggerOrdersInputState +import exchange.dydx.abacus.utils.Numeric +import indexer.codegen.IndexerPositionSide +import kotlin.math.max + +internal class TriggerOrdersInputCalculatorV2() { + fun calculate( + triggerOrders: InternalTriggerOrdersInputState, + account: InternalAccountState, + subaccountNumber: Int, + ): InternalTriggerOrdersInputState { + val subaccount = account.groupedSubaccounts[subaccountNumber] + ?: account.subaccounts[subaccountNumber] + val marketId = triggerOrders.marketId + val inputSize = triggerOrders.size + val stopLossOrder = triggerOrders.stopLossOrder + val takeProfitOrder = triggerOrders.takeProfitOrder + val position = subaccount?.openPositions?.get(marketId) + + if (position != null) { + if (stopLossOrder != null) { + triggerOrders.stopLossOrder = + calculateTriggerOrderTrade(stopLossOrder, triggerOrders.marketId, position, inputSize) + } + if (takeProfitOrder != null) { + triggerOrders.takeProfitOrder = + calculateTriggerOrderTrade(takeProfitOrder, triggerOrders.marketId, position, inputSize) + } + } + + return triggerOrders + } + + private fun calculateTriggerOrderTrade( + triggerOrder: InternalTriggerOrderState, + marketId: String?, + position: InternalPerpetualPosition, + inputSize: Double?, + ): InternalTriggerOrderState { + val orderSize = triggerOrder.size + val absSize = (inputSize ?: orderSize)?.abs() + triggerOrder.price = + triggerOrder.price?.let { calculateTriggerPrices(it, position, absSize) } + + return finalizeOrderFromPriceInputs(triggerOrder, marketId, position, absSize) + } + + private fun calculateTriggerPrices( + triggerPrices: TriggerPrice, + position: InternalPerpetualPosition, + size: Double?, + ): TriggerPrice { + var modified = triggerPrices + + val inputType = triggerPrices.input + val currentPosition = position.calculated[CalculationPeriod.current] + val entryPrice = position.entryPrice + val positionSide = position.side + val positionSize = currentPosition?.size?.abs() ?: return triggerPrices + val notionalTotal = currentPosition.notionalTotal ?: return triggerPrices + val leverage = currentPosition.leverage ?: return triggerPrices + + if (size == null || size == Numeric.double.ZERO || notionalTotal == Numeric.double.ZERO || leverage == Numeric.double.ZERO) { + // A valid position size should never have 0 size, notional value or leverage. + return triggerPrices; + } + + val scaledLeverage = max(leverage.abs(), 1.0) + val scaledNotionalTotal = size.div(positionSize).times(notionalTotal); + + if (entryPrice != null) { + val triggerPrice = triggerPrices.triggerPrice + val usdcDiff = triggerPrices.usdcDiff + val percentDiff = triggerPrices.percentDiff?.let { it / 100.0 } + + when (inputType) { + "stopLossOrder.price.triggerPrice" -> { + if (triggerPrice != null) { + val usdcDiffValue = when (positionSide) { + IndexerPositionSide.LONG -> size.times(entryPrice.minus(triggerPrice)) + IndexerPositionSide.SHORT -> size.times(triggerPrice.minus(entryPrice)) + else -> null + } + modified = modified.copy(usdcDiff = usdcDiffValue) + val percentDiffValue = when (positionSide) { + IndexerPositionSide.LONG -> size.times( + scaledLeverage.times( + entryPrice.minus( + triggerPrice, + ), + ), + ).div(scaledNotionalTotal).times(100) + + IndexerPositionSide.SHORT -> size.times( + scaledLeverage.times( + triggerPrice.minus(entryPrice), + ), + ).div(scaledNotionalTotal).times(100) + + else -> null + } + modified = modified.copy(percentDiff = percentDiffValue) + } else { + modified = modified.copy(usdcDiff = null) + modified = modified.copy(percentDiff = null) + } + } + + "takeProfitOrder.price.triggerPrice" -> { + if (triggerPrice != null) { + val usdcDiffValue = when (positionSide) { + IndexerPositionSide.LONG -> size.times(triggerPrice.minus(entryPrice)) + IndexerPositionSide.SHORT -> size.times(entryPrice.minus(triggerPrice)) + else -> null + } + modified = modified.copy(usdcDiff = usdcDiffValue) + val percentDiffValue = when (positionSide) { + IndexerPositionSide.LONG -> size.times( + scaledLeverage.times( + triggerPrice.minus( + entryPrice, + ), + ), + ).div(scaledNotionalTotal).times(100) + + IndexerPositionSide.SHORT -> size.times( + scaledLeverage.times( + entryPrice.minus( + triggerPrice, + ), + ), + ).div(scaledNotionalTotal).times(100) + + else -> null + } + modified = modified.copy(percentDiff = percentDiffValue) + } else { + modified = modified.copy(usdcDiff = null) + modified = modified.copy(percentDiff = null) + } + } + + "stopLossOrder.price.usdcDiff" -> { + if (usdcDiff != null) { + val triggerPriceValue = when (positionSide) { + IndexerPositionSide.LONG -> entryPrice.minus(usdcDiff.div(size)) + IndexerPositionSide.SHORT -> entryPrice.plus(usdcDiff.div(size)) + else -> null + } + modified = modified.copy(triggerPrice = triggerPriceValue) + val percentDiffValue = + usdcDiff.div(scaledNotionalTotal).times(scaledLeverage).times(100) + modified = modified.copy(percentDiff = percentDiffValue) + } else { + modified = modified.copy(triggerPrice = null) + modified = modified.copy(percentDiff = null) + } + } + + "takeProfitOrder.price.usdcDiff" -> { + if (usdcDiff != null) { + val triggerPriceValue = when (positionSide) { + IndexerPositionSide.LONG -> entryPrice.plus(usdcDiff.div(size)) + IndexerPositionSide.SHORT -> entryPrice.minus(usdcDiff.div(size)) + else -> null + } + modified = modified.copy(triggerPrice = triggerPriceValue) + val percentDiffValue = + usdcDiff.div(scaledNotionalTotal).times(scaledLeverage).times(100) + modified = modified.copy(percentDiff = percentDiffValue) + } else { + modified = modified.copy(triggerPrice = null) + modified = modified.copy(percentDiff = null) + } + } + + "stopLossOrder.price.percentDiff" -> { + if (percentDiff != null) { + val triggerPriceValue = when (positionSide) { + IndexerPositionSide.LONG -> entryPrice.minus( + percentDiff.times( + scaledNotionalTotal, + ).div(scaledLeverage.times(size)), + ) + + IndexerPositionSide.SHORT -> entryPrice.plus( + percentDiff.times( + scaledNotionalTotal, + ).div(scaledLeverage.times(size)), + ) + + else -> null + } + modified = modified.copy(triggerPrice = triggerPriceValue) + val usdcDiffValue = + percentDiff.times(scaledNotionalTotal).div(scaledLeverage) + modified = modified.copy(usdcDiff = usdcDiffValue) + } else { + modified = modified.copy(triggerPrice = null) + modified = modified.copy(usdcDiff = null) + } + } + + "takeProfitOrder.price.percentDiff" -> { + if (percentDiff != null) { + val triggerPriceValue = when (positionSide) { + IndexerPositionSide.LONG -> entryPrice.plus( + percentDiff.times( + scaledNotionalTotal, + ).div(scaledLeverage.times(size)), + ) + + IndexerPositionSide.SHORT -> entryPrice.minus( + percentDiff.times( + scaledNotionalTotal, + ).div(scaledLeverage.times(size)), + ) + + else -> null + } + modified = modified.copy(triggerPrice = triggerPriceValue) + val usdcDiffValue = + percentDiff.times(scaledNotionalTotal).div(scaledLeverage) + modified = modified.copy(usdcDiff = usdcDiffValue) + } else { + modified = modified.copy(triggerPrice = null) + modified = modified.copy(usdcDiff = null) + } + } + + else -> {} + } + } + + return modified + } + + private fun finalizeOrderFromPriceInputs( + triggerOrder: InternalTriggerOrderState, + marketId: String?, + position: InternalPerpetualPosition, + size: Double?, + ): InternalTriggerOrderState { + triggerOrder.side = getOrderSide(position) + triggerOrder.type = getOrderType(triggerOrder) + + val price: Double? = getPrice(triggerOrder, marketId) + triggerOrder.summary = TriggerOrderInputSummary(size = size, price = price) + + return triggerOrder + } + + private fun getOrderSide( + position: InternalPerpetualPosition, + ): OrderSide? { + val positionSide = position.side + + return when (positionSide) { + IndexerPositionSide.SHORT -> OrderSide.Buy + IndexerPositionSide.LONG -> OrderSide.Sell + else -> null + } + } + + private fun getOrderType( + triggerOrder: InternalTriggerOrderState + ): OrderType? { + val limitPrice = triggerOrder.price?.limitPrice + val type = triggerOrder.type + + if (limitPrice != null) { + return when (type) { + OrderType.TakeProfitMarket, OrderType.TakeProfitLimit -> OrderType.TakeProfitLimit + OrderType.StopMarket, OrderType.StopLimit -> OrderType.StopLimit + else -> null + } + } else { + return when (type) { + OrderType.TakeProfitMarket, OrderType.TakeProfitLimit -> OrderType.TakeProfitMarket + OrderType.StopMarket, OrderType.StopLimit -> OrderType.StopMarket + else -> null + } + } + } + + private fun getPrice( + triggerOrder: InternalTriggerOrderState, + marketId: String? + ): Double? { + when (triggerOrder.type) { + OrderType.TakeProfitMarket, OrderType.StopMarket -> { + val triggerPrice = triggerOrder.price?.triggerPrice + val majorMarket = MAJOR_MARKETS.contains(marketId) + val slippagePercentage = if (majorMarket) { + if (triggerOrder.type == OrderType.StopMarket) { + STOP_MARKET_ORDER_SLIPPAGE_BUFFER_MAJOR_MARKET + } else { + TAKE_PROFIT_MARKET_ORDER_SLIPPAGE_BUFFER_MAJOR_MARKET + } + } else { + if (triggerOrder.type == OrderType.StopMarket) { + STOP_MARKET_ORDER_SLIPPAGE_BUFFER + } else { + TAKE_PROFIT_MARKET_ORDER_SLIPPAGE_BUFFER + } + } + val calculatedLimitPrice = if (triggerPrice != null) { + if (triggerOrder.side == OrderSide.Buy) { + triggerPrice * (Numeric.double.ONE + slippagePercentage) + } else { + triggerPrice * (Numeric.double.ONE - slippagePercentage) + } + } else { + null + } + return calculatedLimitPrice + } + OrderType.TakeProfitLimit, OrderType.StopLimit -> { + return triggerOrder.price?.limitPrice + } + else -> { + return null + } + } + } +} 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 92fcb8d52..f0baef2ed 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/output/input/Input.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/Input.kt @@ -73,8 +73,15 @@ data class Input( val transfer = TransferInput.create(existing?.transfer, parser, parser.asMap(data?.get("transfer")), environment, internalState?.transfer) - val triggerOrders = - TriggerOrdersInput.create(existing?.triggerOrders, parser, parser.asMap(data?.get("triggerOrders"))) + val triggerOrders = if (staticTyping) { + TriggerOrdersInput.create(state = internalState?.input?.triggerOrders) + } else { + TriggerOrdersInput.create( + existing?.triggerOrders, + parser, + parser.asMap(data?.get("triggerOrders")), + ) + } val adjustIsolatedMargin = AdjustIsolatedMarginInput.create( diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/output/input/TriggerOrdersInput.kt b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/TriggerOrdersInput.kt index 22c61a00b..5d02dc2fa 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/output/input/TriggerOrdersInput.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/TriggerOrdersInput.kt @@ -1,6 +1,8 @@ package exchange.dydx.abacus.output.input import exchange.dydx.abacus.protocols.ParserProtocol +import exchange.dydx.abacus.state.internalstate.InternalTriggerOrderState +import exchange.dydx.abacus.state.internalstate.InternalTriggerOrdersInputState import exchange.dydx.abacus.utils.Logger import kollections.JsExport import kotlinx.serialization.Serializable @@ -87,6 +89,23 @@ data class TriggerOrder( val summary: TriggerOrderInputSummary?, ) { companion object { + internal fun create( + state: InternalTriggerOrderState? + ): TriggerOrder? { + return if (state != null) { + TriggerOrder( + orderId = state.orderId, + size = state.size, + type = state.type, + side = state.side, + price = state.price, + summary = state.summary, + ) + } else { + null + } + } + internal fun create( existing: TriggerOrder?, parser: ParserProtocol, @@ -143,6 +162,21 @@ data class TriggerOrdersInput( val takeProfitOrder: TriggerOrder?, ) { companion object { + internal fun create( + state: InternalTriggerOrdersInputState? + ): TriggerOrdersInput? { + return if (state != null) { + TriggerOrdersInput( + marketId = state.marketId, + size = state.size, + stopLossOrder = TriggerOrder.create(state.stopLossOrder), + takeProfitOrder = TriggerOrder.create(state.takeProfitOrder), + ) + } else { + null + } + } + internal fun create( existing: TriggerOrdersInput?, parser: ParserProtocol, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/ClosePositionInputProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/ClosePositionInputProcessor.kt index 35ecf3d25..69df0722c 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/ClosePositionInputProcessor.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/ClosePositionInputProcessor.kt @@ -26,13 +26,24 @@ import exchange.dydx.abacus.state.model.ClosePositionInputField import exchange.dydx.abacus.utils.Numeric import kollections.iListOf -internal interface ClosePositionInputProcessorProtocol +internal interface ClosePositionInputProcessorProtocol { + fun closePosition( + inputState: InternalInputState, + walletState: InternalWalletState, + marketSummaryState: InternalMarketSummaryState, + configs: InternalConfigsState, + rewardsParams: InternalRewardsParamsState?, + data: String?, + type: ClosePositionInputField, + subaccountNumber: Int, + ): TradeInputResult +} internal class ClosePositionInputProcessor( private val parser: ParserProtocol, ) : ClosePositionInputProcessorProtocol { - fun closePosition( + override fun closePosition( inputState: InternalInputState, walletState: InternalWalletState, marketSummaryState: InternalMarketSummaryState, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TriggerOrdersInputProcessor.kt b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TriggerOrdersInputProcessor.kt new file mode 100644 index 000000000..c58206704 --- /dev/null +++ b/src/commonMain/kotlin/exchange.dydx.abacus/processor/input/TriggerOrdersInputProcessor.kt @@ -0,0 +1,238 @@ +package exchange.dydx.abacus.processor.input + +import exchange.dydx.abacus.calculator.v2.TriggerOrdersInputCalculatorV2 +import exchange.dydx.abacus.output.input.InputType +import exchange.dydx.abacus.output.input.OrderType +import exchange.dydx.abacus.output.input.TriggerPrice +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.InternalTriggerOrderState +import exchange.dydx.abacus.state.internalstate.InternalTriggerOrdersInputState +import exchange.dydx.abacus.state.internalstate.safeCreate +import exchange.dydx.abacus.state.model.TriggerOrdersInputField +import kollections.iListOf + +internal interface TriggerOrdersInputProcessorProtocol { + fun triggerOrderInput( + inputState: InternalInputState, + account: InternalAccountState, + data: String?, + type: TriggerOrdersInputField?, + subaccountNumber: Int, + ): StateChanges +} + +internal class TriggerOrdersInputProcessor( + private val parser: ParserProtocol, + private val calculator: TriggerOrdersInputCalculatorV2 = TriggerOrdersInputCalculatorV2() +) : TriggerOrdersInputProcessorProtocol { + + override fun triggerOrderInput( + inputState: InternalInputState, + account: InternalAccountState, + data: String?, + type: TriggerOrdersInputField?, + subaccountNumber: Int, + ): StateChanges { + if (inputState.currentType != InputType.TRIGGER_ORDERS) { + inputState.currentType = InputType.TRIGGER_ORDERS + inputState.triggerOrders = InternalTriggerOrdersInputState() + calculator.calculate(inputState.triggerOrders, account, subaccountNumber) + } + + if (type != null) { + when (type) { + TriggerOrdersInputField.marketId -> { + inputState.triggerOrders.marketId = parser.asString(data) + } + + TriggerOrdersInputField.stopLossOrderId -> { + inputState.triggerOrders.stopLossOrder = + inputState.triggerOrders.stopLossOrder ?: InternalTriggerOrderState() + inputState.triggerOrders.stopLossOrder?.orderId = parser.asString(data) + } + + TriggerOrdersInputField.takeProfitOrderId -> { + inputState.triggerOrders.takeProfitOrder = + inputState.triggerOrders.takeProfitOrder ?: InternalTriggerOrderState() + inputState.triggerOrders.takeProfitOrder?.orderId = parser.asString(data) + } + + TriggerOrdersInputField.stopLossOrderType -> { + inputState.triggerOrders.stopLossOrder = + inputState.triggerOrders.stopLossOrder ?: InternalTriggerOrderState() + inputState.triggerOrders.stopLossOrder?.type = + OrderType.invoke(parser.asString(data)) + } + + TriggerOrdersInputField.takeProfitOrderType -> { + inputState.triggerOrders.takeProfitOrder = + inputState.triggerOrders.takeProfitOrder ?: InternalTriggerOrderState() + inputState.triggerOrders.takeProfitOrder?.type = + OrderType.invoke(parser.asString(data)) + } + + TriggerOrdersInputField.size -> { + inputState.triggerOrders.size = parser.asDouble(data) + } + + TriggerOrdersInputField.stopLossOrderSize -> { + inputState.triggerOrders.stopLossOrder = + inputState.triggerOrders.stopLossOrder ?: InternalTriggerOrderState() + inputState.triggerOrders.stopLossOrder?.size = parser.asDouble(data) + } + + TriggerOrdersInputField.takeProfitOrderSize -> { + inputState.triggerOrders.takeProfitOrder = + inputState.triggerOrders.takeProfitOrder ?: InternalTriggerOrderState() + inputState.triggerOrders.takeProfitOrder?.size = parser.asDouble(data) + } + + TriggerOrdersInputField.stopLossLimitPrice -> { + inputState.triggerOrders.stopLossOrder = + inputState.triggerOrders.stopLossOrder ?: InternalTriggerOrderState() + var price = + TriggerPrice.safeCreate(inputState.triggerOrders.stopLossOrder?.price) + price = price.copy( + limitPrice = parser.asDouble(data), + ) + inputState.triggerOrders.stopLossOrder?.price = price + } + + TriggerOrdersInputField.takeProfitLimitPrice -> { + inputState.triggerOrders.takeProfitOrder = + inputState.triggerOrders.takeProfitOrder ?: InternalTriggerOrderState() + var price = + TriggerPrice.safeCreate(inputState.triggerOrders.takeProfitOrder?.price) + price = price.copy( + limitPrice = parser.asDouble(data), + ) + inputState.triggerOrders.takeProfitOrder?.price = price + } + + TriggerOrdersInputField.stopLossPrice -> { + inputState.triggerOrders.stopLossOrder = + inputState.triggerOrders.stopLossOrder ?: InternalTriggerOrderState() + val newValue = parser.asDouble(data) + val stopLossPriceChanged = + (newValue != inputState.triggerOrders.stopLossOrder?.price?.triggerPrice) + var price = + TriggerPrice.safeCreate(inputState.triggerOrders.stopLossOrder?.price) + price = price.copy( + triggerPrice = newValue, + ) + if (stopLossPriceChanged) { + price = price.copy( + input = type.rawValue, + ) + } + inputState.triggerOrders.stopLossOrder?.price = price + } + + TriggerOrdersInputField.stopLossPercentDiff -> { + inputState.triggerOrders.stopLossOrder = + inputState.triggerOrders.stopLossOrder ?: InternalTriggerOrderState() + val newValue = parser.asDouble(data) + val stopLossPriceChanged = + (newValue != inputState.triggerOrders.stopLossOrder?.price?.percentDiff) + var price = + TriggerPrice.safeCreate(inputState.triggerOrders.stopLossOrder?.price) + price = price.copy( + percentDiff = newValue, + ) + if (stopLossPriceChanged) { + price = price.copy( + input = type.rawValue, + ) + } + inputState.triggerOrders.stopLossOrder?.price = price + } + + TriggerOrdersInputField.stopLossUsdcDiff -> { + inputState.triggerOrders.stopLossOrder = + inputState.triggerOrders.stopLossOrder ?: InternalTriggerOrderState() + val newValue = parser.asDouble(data) + val stopLossPriceChanged = + (newValue != inputState.triggerOrders.stopLossOrder?.price?.usdcDiff) + var price = + TriggerPrice.safeCreate(inputState.triggerOrders.stopLossOrder?.price) + price = price.copy( + usdcDiff = newValue, + ) + if (stopLossPriceChanged) { + price = price.copy( + input = type.rawValue, + ) + } + inputState.triggerOrders.stopLossOrder?.price = price + } + + TriggerOrdersInputField.takeProfitPrice -> { + inputState.triggerOrders.takeProfitOrder = + inputState.triggerOrders.takeProfitOrder ?: InternalTriggerOrderState() + val newValue = parser.asDouble(data) + val takeProfitPriceChanged = + (newValue != inputState.triggerOrders.takeProfitOrder?.price?.triggerPrice) + var price = + TriggerPrice.safeCreate(inputState.triggerOrders.takeProfitOrder?.price) + price = price.copy( + triggerPrice = newValue, + ) + if (takeProfitPriceChanged) { + price = price.copy( + input = type.rawValue, + ) + } + inputState.triggerOrders.takeProfitOrder?.price = price + } + + TriggerOrdersInputField.takeProfitPercentDiff -> { + inputState.triggerOrders.takeProfitOrder = + inputState.triggerOrders.takeProfitOrder ?: InternalTriggerOrderState() + val newValue = parser.asDouble(data) + val takeProfitPriceChanged = + (newValue != inputState.triggerOrders.takeProfitOrder?.price?.percentDiff) + var price = + TriggerPrice.safeCreate(inputState.triggerOrders.takeProfitOrder?.price) + price = price.copy( + percentDiff = newValue, + ) + if (takeProfitPriceChanged) { + price = price.copy( + input = type.rawValue, + ) + } + inputState.triggerOrders.takeProfitOrder?.price = price + } + + TriggerOrdersInputField.takeProfitUsdcDiff -> { + inputState.triggerOrders.takeProfitOrder = + inputState.triggerOrders.takeProfitOrder ?: InternalTriggerOrderState() + val newValue = parser.asDouble(data) + val takeProfitPriceChanged = + (newValue != inputState.triggerOrders.takeProfitOrder?.price?.usdcDiff) + var price = + TriggerPrice.safeCreate(inputState.triggerOrders.takeProfitOrder?.price) + price = price.copy( + usdcDiff = newValue, + ) + if (takeProfitPriceChanged) { + price = price.copy( + input = type.rawValue, + ) + } + inputState.triggerOrders.takeProfitOrder?.price = price + } + } + } + + return StateChanges( + changes = iListOf(Changes.input), + markets = null, + subaccountNumbers = iListOf(subaccountNumber), + ) + } +} 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 86e78812f..4e6f1c55a 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,8 @@ 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.TriggerOrderInputSummary +import exchange.dydx.abacus.output.input.TriggerPrice import exchange.dydx.abacus.output.input.ValidationError import exchange.dydx.abacus.state.manager.HistoricalTradingRewardsPeriod import exchange.dydx.abacus.utils.NUM_PARENT_SUBACCOUNTS @@ -56,6 +58,7 @@ internal data class InternalState( internal data class InternalInputState( var trade: InternalTradeInputState = InternalTradeInputState(), var closePosition: InternalTradeInputState = InternalTradeInputState(), + var triggerOrders: InternalTriggerOrdersInputState = InternalTriggerOrdersInputState(), var receiptLines: List? = null, var errors: List? = null, var childSubaccountErrors: List? = null, @@ -71,6 +74,22 @@ internal data class InternalInputState( } } +internal data class InternalTriggerOrdersInputState( + var marketId: String? = null, + var size: Double? = null, + var stopLossOrder: InternalTriggerOrderState? = null, + var takeProfitOrder: InternalTriggerOrderState? = null, +) + +internal data class InternalTriggerOrderState( + var orderId: String? = null, + var size: Double? = null, + var type: OrderType? = null, + var side: OrderSide? = null, + var price: TriggerPrice? = null, + var summary: TriggerOrderInputSummary? = null, +) + internal data class InternalTradeInputState( var marketId: String? = null, var size: TradeInputSize? = null, @@ -336,6 +355,7 @@ internal data class InternalPerpetualPosition( // Calculated: val calculated: MutableMap = mutableMapOf(), + var childSubaccountNumber: Int? = null, ) { val marginMode: MarginMode? get() { @@ -433,3 +453,13 @@ internal fun TradeInputGoodUntil.Companion.safeCreate(existing: TradeInputGoodUn unit = null, ) } + +internal fun TriggerPrice.Companion.safeCreate(existing: TriggerPrice?): TriggerPrice { + return existing ?: TriggerPrice( + limitPrice = null, + triggerPrice = null, + percentDiff = null, + usdcDiff = null, + input = 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 3eac9a0c9..eb9f5d97c 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 @@ -1,6 +1,7 @@ package exchange.dydx.abacus.state.model import exchange.dydx.abacus.calculator.TriggerOrdersInputCalculator +import exchange.dydx.abacus.processor.input.TriggerOrdersInputProcessor import exchange.dydx.abacus.responses.ParsingError import exchange.dydx.abacus.responses.StateResponse import exchange.dydx.abacus.responses.cannotModify @@ -37,7 +38,7 @@ enum class TriggerOrdersInputField(val rawValue: String) { companion object { operator fun invoke(rawValue: String) = - TriggerOrdersInputField.values().firstOrNull { it.rawValue == rawValue } + entries.firstOrNull { it.rawValue == rawValue } } } @@ -46,128 +47,166 @@ fun TradingStateMachine.triggerOrders( type: TriggerOrdersInputField?, subaccountNumber: Int, ): StateResponse { - var changes: StateChanges? = null - var error: ParsingError? = null - val typeText = type?.rawValue - - val input = this.input?.mutable() ?: mutableMapOf() - input["current"] = "triggerOrders" - val triggerOrders = - parser.asMap(input["triggerOrders"])?.mutable() - ?: kotlin.run { - val triggerOrders = mutableMapOf() - - val calculator = TriggerOrdersInputCalculator(parser) - val params = mutableMapOf() - params.safeSet("triggerOrders", triggerOrders) - val modified = calculator.calculate(params, subaccountNumber) - - parser.asMap(modified["triggerOrders"])?.mutable() ?: triggerOrders - } + if (staticTyping) { + val triggerOrdersInputProcessor = TriggerOrdersInputProcessor(parser) + val changes = triggerOrdersInputProcessor.triggerOrderInput( + inputState = internalState.input, + account = internalState.wallet.account, + data = data, + type = type, + subaccountNumber = subaccountNumber, + ) + updateStateChanges(changes) + return StateResponse( + state = state, + changes = changes, + errors = null, + ) + } else { + var changes: StateChanges? = null + var error: ParsingError? = null + val typeText = type?.rawValue - var stopLossPriceChanged = false - var takeProfitPriceChanged = false + val input = this.input?.mutable() ?: mutableMapOf() + input["current"] = "triggerOrders" + val triggerOrders = + parser.asMap(input["triggerOrders"])?.mutable() + ?: kotlin.run { + val triggerOrders = mutableMapOf() - if (typeText != null) { - if (validTriggerOrdersInput(triggerOrders, typeText)) { - when (typeText) { - TriggerOrdersInputField.marketId.rawValue -> { - triggerOrders.safeSet(typeText, parser.asString(data)) - changes = - StateChanges( - iListOf(Changes.input), - null, - iListOf(subaccountNumber), - ) - } - TriggerOrdersInputField.stopLossOrderId.rawValue, - TriggerOrdersInputField.takeProfitOrderId.rawValue, - TriggerOrdersInputField.stopLossOrderType.rawValue, - TriggerOrdersInputField.takeProfitOrderType.rawValue -> { - triggerOrders.safeSet(typeText, parser.asString(data)) - changes = - StateChanges( - iListOf(Changes.input), - null, - iListOf(subaccountNumber), - ) + val calculator = TriggerOrdersInputCalculator(parser) + val params = mutableMapOf() + params.safeSet("triggerOrders", triggerOrders) + val modified = calculator.calculate(params, subaccountNumber) + + parser.asMap(modified["triggerOrders"])?.mutable() ?: triggerOrders } - TriggerOrdersInputField.size.rawValue, - TriggerOrdersInputField.stopLossOrderSize.rawValue, - TriggerOrdersInputField.takeProfitOrderSize.rawValue, - TriggerOrdersInputField.stopLossLimitPrice.rawValue, - TriggerOrdersInputField.takeProfitLimitPrice.rawValue -> { - triggerOrders.safeSet(typeText, parser.asDouble(data)) - changes = - StateChanges( - iListOf(Changes.input), - null, - iListOf(subaccountNumber), - ) + + var stopLossPriceChanged = false + var takeProfitPriceChanged = false + + if (typeText != null) { + if (validTriggerOrdersInput(triggerOrders, typeText)) { + when (typeText) { + TriggerOrdersInputField.marketId.rawValue -> { + triggerOrders.safeSet(typeText, parser.asString(data)) + changes = + StateChanges( + iListOf(Changes.input), + null, + iListOf(subaccountNumber), + ) + } + + TriggerOrdersInputField.stopLossOrderId.rawValue, + TriggerOrdersInputField.takeProfitOrderId.rawValue, + TriggerOrdersInputField.stopLossOrderType.rawValue, + TriggerOrdersInputField.takeProfitOrderType.rawValue -> { + triggerOrders.safeSet(typeText, parser.asString(data)) + changes = + StateChanges( + iListOf(Changes.input), + null, + iListOf(subaccountNumber), + ) + } + + TriggerOrdersInputField.size.rawValue, + TriggerOrdersInputField.stopLossOrderSize.rawValue, + TriggerOrdersInputField.takeProfitOrderSize.rawValue, + TriggerOrdersInputField.stopLossLimitPrice.rawValue, + TriggerOrdersInputField.takeProfitLimitPrice.rawValue -> { + triggerOrders.safeSet(typeText, parser.asDouble(data)) + changes = + StateChanges( + iListOf(Changes.input), + null, + iListOf(subaccountNumber), + ) + } + + TriggerOrdersInputField.stopLossPrice.rawValue, + TriggerOrdersInputField.stopLossPercentDiff.rawValue, + TriggerOrdersInputField.stopLossUsdcDiff.rawValue -> { + stopLossPriceChanged = + ( + parser.asDouble(data) != parser.asDouble( + parser.value( + triggerOrders, + typeText, + ), + ) + ) + triggerOrders.safeSet(typeText, parser.asDouble(data)) + changes = + StateChanges( + iListOf(Changes.input), + null, + iListOf(subaccountNumber), + ) + } + + TriggerOrdersInputField.takeProfitPrice.rawValue, + TriggerOrdersInputField.takeProfitPercentDiff.rawValue, + TriggerOrdersInputField.takeProfitUsdcDiff.rawValue -> { + takeProfitPriceChanged = + ( + parser.asDouble(data) != parser.asDouble( + parser.value( + triggerOrders, + typeText, + ), + ) + ) + triggerOrders.safeSet(typeText, parser.asDouble(data)) + changes = + StateChanges( + iListOf(Changes.input), + null, + iListOf(subaccountNumber), + ) + } + + else -> {} } + } else { + error = ParsingError.cannotModify(typeText) + } + } else { + changes = + StateChanges( + iListOf(Changes.input), + null, + iListOf(subaccountNumber), + ) + } + + if (stopLossPriceChanged) { + when (typeText) { TriggerOrdersInputField.stopLossPrice.rawValue, TriggerOrdersInputField.stopLossPercentDiff.rawValue, - TriggerOrdersInputField.stopLossUsdcDiff.rawValue -> { - stopLossPriceChanged = - (parser.asDouble(data) != parser.asDouble(parser.value(triggerOrders, typeText))) - triggerOrders.safeSet(typeText, parser.asDouble(data)) - changes = - StateChanges( - iListOf(Changes.input), - null, - iListOf(subaccountNumber), - ) + TriggerOrdersInputField.stopLossUsdcDiff.rawValue, + -> { + triggerOrders.safeSet("stopLossOrder.price.input", typeText) } + } + } + if (takeProfitPriceChanged) { + when (typeText) { TriggerOrdersInputField.takeProfitPrice.rawValue, TriggerOrdersInputField.takeProfitPercentDiff.rawValue, - TriggerOrdersInputField.takeProfitUsdcDiff.rawValue -> { - takeProfitPriceChanged = - (parser.asDouble(data) != parser.asDouble(parser.value(triggerOrders, typeText))) - triggerOrders.safeSet(typeText, parser.asDouble(data)) - changes = - StateChanges( - iListOf(Changes.input), - null, - iListOf(subaccountNumber), - ) + TriggerOrdersInputField.takeProfitUsdcDiff.rawValue, + -> { + triggerOrders.safeSet("takeProfitOrder.price.input", typeText) } - else -> {} } - } else { - error = ParsingError.cannotModify(typeText) } - } else { - changes = - StateChanges( - iListOf(Changes.input), - null, - iListOf(subaccountNumber), - ) - } - if (stopLossPriceChanged) { - when (typeText) { - TriggerOrdersInputField.stopLossPrice.rawValue, - TriggerOrdersInputField.stopLossPercentDiff.rawValue, - TriggerOrdersInputField.stopLossUsdcDiff.rawValue, -> { - triggerOrders.safeSet("stopLossOrder.price.input", typeText) - } - } + input["triggerOrders"] = triggerOrders + this.input = input + changes?.let { updateStateChanges(it) } + return StateResponse(state, changes, if (error != null) iListOf(error) else null) } - if (takeProfitPriceChanged) { - when (typeText) { - TriggerOrdersInputField.takeProfitPrice.rawValue, - TriggerOrdersInputField.takeProfitPercentDiff.rawValue, - TriggerOrdersInputField.takeProfitUsdcDiff.rawValue, -> { - triggerOrders.safeSet("takeProfitOrder.price.input", typeText) - } - } - } - - input["triggerOrders"] = triggerOrders - this.input = input - changes?.let { updateStateChanges(it) } - return StateResponse(state, changes, if (error != null) iListOf(error) else null) } fun TradingStateMachine.validTriggerOrdersInput( 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 dc2ea9bd7..6f321f268 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine.kt @@ -10,6 +10,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.TriggerOrdersInputCalculatorV2 import exchange.dydx.abacus.calculator.v2.tradeinput.TradeInputCalculatorV2 import exchange.dydx.abacus.output.Asset import exchange.dydx.abacus.output.Configs @@ -758,22 +759,21 @@ open class TradingStateMachine( private fun calculateTrade(tag: String, calculation: TradeCalculation, subaccountNumber: Int) { if (staticTyping) { val calculator = TradeInputCalculatorV2(parser, calculation) - 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 - }, - ) + 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)) @@ -818,20 +818,29 @@ open class TradingStateMachine( this.input = input } - private fun calculateTriggerOrders(subaccountNumber: Int?) { - val input = this.input?.mutable() - val triggerOrders = parser.asNativeMap(input?.get("triggerOrders")) - val calculator = TriggerOrdersInputCalculator(parser) - val params = mutableMapOf() - params.safeSet("account", account) - params.safeSet("user", user) - params.safeSet("markets", parser.asNativeMap(marketsSummary?.get("markets"))) - params.safeSet("triggerOrders", triggerOrders) + private fun calculateTriggerOrders(subaccountNumber: Int) { + if (staticTyping) { + val calculator = TriggerOrdersInputCalculatorV2() + calculator.calculate( + triggerOrders = internalState.input.triggerOrders, + account = internalState.wallet.account, + subaccountNumber = subaccountNumber, + ) + } else { + val input = this.input?.mutable() + val triggerOrders = parser.asNativeMap(input?.get("triggerOrders")) + val calculator = TriggerOrdersInputCalculator(parser) + val params = mutableMapOf() + params.safeSet("account", account) + params.safeSet("user", user) + params.safeSet("markets", parser.asNativeMap(marketsSummary?.get("markets"))) + params.safeSet("triggerOrders", triggerOrders) - val modified = calculator.calculate(params, subaccountNumber) - input?.safeSet("triggerOrders", parser.asNativeMap(modified["triggerOrders"])) + val modified = calculator.calculate(params, subaccountNumber) + input?.safeSet("triggerOrders", parser.asNativeMap(modified["triggerOrders"])) - this.input = input + this.input = input + } } private fun calculateAdjustIsolatedMargin(subaccountNumber: Int?) { 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 681354612..a748f96f9 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 @@ -266,7 +266,17 @@ internal class SubaccountTransactionPayloadProvider( val goodTilTimeInSeconds = if (isLimitClose) (limitCloseDuration / 1.seconds).toInt() else null val goodTilBlock = if (isLimitClose) null else currentHeight?.plus(SHORT_TERM_ORDER_DURATION) val marketInfo = marketInfo(marketId) - val subaccountNumberForPosition = helper.parser.asInt(helper.parser.value(stateMachine.data, "wallet.account.groupedSubaccounts.$subaccountNumber.openPositions.$marketId.childSubaccountNumber")) ?: subaccountNumber + val subaccountNumberForPosition = + if (stateMachine.staticTyping) { + stateMachine.internalState.wallet.account.groupedSubaccounts[subaccountNumber]?.openPositions?.get(marketId)?.childSubaccountNumber ?: subaccountNumber + } else { + helper.parser.asInt( + helper.parser.value( + stateMachine.data, + "wallet.account.groupedSubaccounts.$subaccountNumber.openPositions.$marketId.childSubaccountNumber", + ), + ) ?: subaccountNumber + } return HumanReadablePlaceOrderPayload( subaccountNumber = subaccountNumberForPosition, diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/TriggerOrdersInputValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/TriggerOrdersInputValidator.kt index 38bd13989..8d3047470 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/TriggerOrdersInputValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/TriggerOrdersInputValidator.kt @@ -1,5 +1,8 @@ package exchange.dydx.abacus.validator + import abs +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 @@ -7,7 +10,11 @@ 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.InternalTriggerOrderState +import exchange.dydx.abacus.state.internalstate.InternalTriggerOrdersInputState import exchange.dydx.abacus.state.manager.BlockAndTime import exchange.dydx.abacus.state.manager.V4Environment import exchange.dydx.abacus.state.model.TriggerOrdersInputField @@ -18,7 +25,7 @@ enum class RelativeToPrice(val rawValue: String) { companion object { operator fun invoke(rawValue: String) = - values().firstOrNull { it.rawValue == rawValue } + entries.firstOrNull { it.rawValue == rawValue } } } @@ -35,7 +42,39 @@ internal class TriggerOrdersInputValidator( inputType: InputType, environment: V4Environment?, ): List? { - return null + if (inputType != InputType.TRIGGER_ORDERS) { + return null + } + val errors = mutableListOf() + + val triggerOrders = internalState.input.triggerOrders + val marketId = triggerOrders.marketId ?: return null + val market = internalState.marketsSummary.markets[marketId] + val subaccountNumber = subaccountNumber ?: return null + val subaccount = internalState.wallet.account.subaccounts[subaccountNumber] ?: return null + val position = subaccount.openPositions?.get(marketId) ?: return null + val tickSize = market?.perpetualMarket?.configs?.tickSize ?: 0.01 + val oraclePrice = market?.perpetualMarket?.oraclePrice ?: return null + + validateTriggerOrders(triggerOrders, market)?.let { + errors.addAll(it) + } + + val takeProfitOrder = triggerOrders.takeProfitOrder + val stopLossOrder = triggerOrders.stopLossOrder + + if (takeProfitOrder != null) { + validateTriggerOrder(takeProfitOrder, market, oraclePrice, tickSize, position)?.let { + errors.addAll(it) + } + } + if (stopLossOrder != null) { + validateTriggerOrder(stopLossOrder, market, oraclePrice, tickSize, position)?.let { + errors.addAll(it) + } + } + + return if (errors.size > 0) errors else null } override fun validateDeprecated( @@ -64,7 +103,7 @@ internal class TriggerOrdersInputValidator( ), ) ?: return null - validateTriggerOrders(transaction, market)?.let { + validateTriggerOrdersDeprecated(transaction, market)?.let { errors.addAll(it) } @@ -72,7 +111,7 @@ internal class TriggerOrdersInputValidator( val stopLossOrder = parser.asMap(transaction["stopLossOrder"]) val takeProfitError = if (takeProfitOrder != null) { - validateTriggerOrder( + validateTriggerOrderDeprecated( takeProfitOrder, market, oraclePrice, @@ -88,7 +127,7 @@ internal class TriggerOrdersInputValidator( } val stopLossError = if (stopLossOrder != null) { - validateTriggerOrder( + validateTriggerOrderDeprecated( stopLossOrder, market, oraclePrice, @@ -109,11 +148,27 @@ internal class TriggerOrdersInputValidator( } private fun validateTriggerOrders( + triggerOrders: InternalTriggerOrdersInputState, + market: InternalMarketState? + ): List? { + val errors = mutableListOf() + + validateSize(triggerOrders.size, market)?.let { + /* + ORDER_SIZE_BELOW_MIN_SIZE + */ + errors.addAll(it) + } + + return if (errors.size > 0) errors else null + } + + private fun validateTriggerOrdersDeprecated( triggerOrders: Map, market: Map?, ): MutableList? { val triggerErrors = mutableListOf() - validateSize(parser.asDouble(triggerOrders["size"]), market)?.let { + validateSizeDeprecated(parser.asDouble(triggerOrders["size"]), market)?.let { /* ORDER_SIZE_BELOW_MIN_SIZE */ @@ -123,6 +178,63 @@ internal class TriggerOrdersInputValidator( } private fun validateTriggerOrder( + triggerOrder: InternalTriggerOrderState, + market: InternalMarketState?, + oraclePrice: Double, + tickSize: Double, + position: InternalPerpetualPosition, + ): List? { + val triggerErrors = mutableListOf() + + validateRequiredInput(triggerOrder)?.let { + /* + REQUIRED_TRIGGER_PRICE + */ + triggerErrors.addAll(it) + } + + validateSize(triggerOrder.summary?.size, market)?.let { + /* + ORDER_SIZE_BELOW_MIN_SIZE + */ + triggerErrors.addAll(it) + } + + validateTriggerPrice(triggerOrder, oraclePrice, tickSize.toString())?.let { + /* + TRIGGER_MUST_ABOVE_INDEX_PRICE + TRIGGER_MUST_BELOW_INDEX_PRICE + */ + triggerErrors.addAll(it) + } + + validateLimitPrice(triggerOrder)?.let { + /* + LIMIT_MUST_ABOVE_TRIGGER_PRICE + LIMIT_MUST_BELOW_TRIGGER_PRICE + */ + triggerErrors.addAll(it) + } + + validateCalculatedPricesPositive(triggerOrder)?.let { + /* + PRICE_MUST_POSITIVE + */ + triggerErrors.addAll(it) + } + + validateTriggerToLiquidationPrice(triggerOrder, position, tickSize.toString())?.let { + /* + SELL_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE + BUY_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE + */ + triggerErrors.addAll(it) + } + + return if (triggerErrors.size > 0) triggerErrors else null + } + + private fun validateTriggerOrderDeprecated( triggerOrder: Map, market: Map?, oraclePrice: Double, @@ -131,40 +243,40 @@ internal class TriggerOrdersInputValidator( ): MutableList? { val triggerErrors = mutableListOf() - validateRequiredInput(triggerOrder)?.let { + validateRequiredInputDeprecated(triggerOrder)?.let { /* REQUIRED_TRIGGER_PRICE */ triggerErrors.addAll(it) } - validateSize(parser.asDouble(parser.value(triggerOrder, "summary.size")), market)?.let { + validateSizeDeprecated(parser.asDouble(parser.value(triggerOrder, "summary.size")), market)?.let { /* ORDER_SIZE_BELOW_MIN_SIZE */ triggerErrors.addAll(it) } - validateTriggerPrice(triggerOrder, oraclePrice, tickSize)?.let { + validateTriggerPriceDeprecated(triggerOrder, oraclePrice, tickSize)?.let { /* TRIGGER_MUST_ABOVE_INDEX_PRICE TRIGGER_MUST_BELOW_INDEX_PRICE */ triggerErrors.addAll(it) } - validateLimitPrice(triggerOrder)?.let { + validateLimitPriceDeprecated(triggerOrder)?.let { /* LIMIT_MUST_ABOVE_TRIGGER_PRICE LIMIT_MUST_BELOW_TRIGGER_PRICE */ triggerErrors.addAll(it) } - validateCalculatedPricesPositive(triggerOrder)?.let { + validateCalculatedPricesPositiveDeprecated(triggerOrder)?.let { /* PRICE_MUST_POSITIVE */ triggerErrors.addAll(it) } - validateTriggerToLiquidationPrice(triggerOrder, position, tickSize)?.let { + validateTriggerToLiquidationPriceDeprecated(triggerOrder, position, tickSize)?.let { /* SELL_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE BUY_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE @@ -176,6 +288,49 @@ internal class TriggerOrdersInputValidator( } private fun validateTriggerToLiquidationPrice( + triggerOrder: InternalTriggerOrderState, + position: InternalPerpetualPosition, + tickSize: String, + ): List? { + val liquidationPrice = position.calculated[CalculationPeriod.current]?.liquidationPrice ?: return null + val triggerPrice = triggerOrder.price?.triggerPrice ?: return null + val type = triggerOrder.type + val side = triggerOrder.side ?: return null + + return when (requiredTriggerToLiquidationPrice(type, side)) { + RelativeToPrice.ABOVE -> { + if (triggerPrice <= liquidationPrice) { + liquidationPriceError( + errorCode = "SELL_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE", + titleStringKey = "ERRORS.TRIGGERS_FORM_TITLE.SELL_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE", + textStringKey = "ERRORS.TRIGGERS_FORM.SELL_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE_NO_LIMIT", + liquidationPrice = liquidationPrice, + tickSize = tickSize, + ) + } else { + null + } + } + + RelativeToPrice.BELOW -> { + if (triggerPrice >= liquidationPrice) { + liquidationPriceError( + errorCode = "BUY_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE", + titleStringKey = "ERRORS.TRIGGERS_FORM_TITLE.BUY_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE", + textStringKey = "ERRORS.TRIGGERS_FORM.BUY_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE_NO_LIMIT", + liquidationPrice = liquidationPrice, + tickSize = tickSize, + ) + } else { + null + } + } + + else -> null + } + } + + private fun validateTriggerToLiquidationPriceDeprecated( triggerOrder: Map, position: Map, tickSize: String, @@ -192,7 +347,7 @@ internal class TriggerOrdersInputValidator( return when (requiredTriggerToLiquidationPrice(type, side)) { RelativeToPrice.ABOVE -> { if (triggerPrice <= liquidationPrice) { - liquidationPriceError( + liquidationPriceErrorDeprecated( "SELL_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE", "ERRORS.TRIGGERS_FORM_TITLE.SELL_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE", "ERRORS.TRIGGERS_FORM.SELL_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE_NO_LIMIT", @@ -206,7 +361,7 @@ internal class TriggerOrdersInputValidator( RelativeToPrice.BELOW -> { if (triggerPrice >= liquidationPrice) { - liquidationPriceError( + liquidationPriceErrorDeprecated( "BUY_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE", "ERRORS.TRIGGERS_FORM_TITLE.BUY_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE", "ERRORS.TRIGGERS_FORM.BUY_TRIGGER_TOO_CLOSE_TO_LIQUIDATION_PRICE_NO_LIMIT", @@ -228,6 +383,32 @@ internal class TriggerOrdersInputValidator( textStringKey: String, liquidationPrice: Double?, tickSize: String, + ): List? { + return listOf( + error( + type = ErrorType.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", + "tickSize" to tickSize, + ), + ), + ), + ) + } + + private fun liquidationPriceErrorDeprecated( + errorCode: String, + titleStringKey: String, + textStringKey: String, + liquidationPrice: Double?, + tickSize: String, ): List? { return listOf( errorDeprecated( @@ -249,6 +430,25 @@ internal class TriggerOrdersInputValidator( } private fun validateRequiredInput( + triggerOrder: InternalTriggerOrderState, + ): List? { + val triggerPrice = triggerOrder.price?.triggerPrice + val limitPrice = triggerOrder.price?.limitPrice + + if (triggerPrice == null && limitPrice != null) { + return listOf( + required( + errorCode = "REQUIRED_TRIGGER_PRICE", + field = "price.triggerPrice", + actionStringKey = "APP.TRADE.ENTER_TRIGGER_PRICE", + ), + ) + } + + return null + } + + private fun validateRequiredInputDeprecated( triggerOrder: Map, ): List? { val errors = mutableListOf>() @@ -270,6 +470,49 @@ internal class TriggerOrdersInputValidator( } private fun validateCalculatedPricesPositive( + triggerOrder: InternalTriggerOrderState, + ): List? { + val type = triggerOrder.type + val triggerPrice = triggerOrder.price?.triggerPrice + val limitPrice = triggerOrder.price?.limitPrice + val inputField = triggerOrder.price?.input + val fields = if (type == OrderType.StopLimit || type == OrderType.StopMarket) { + if (triggerPrice != null && triggerPrice <= 0) { + listOfNotNull(inputField) + } else if (limitPrice != null && limitPrice <= 0) { + listOf(TriggerOrdersInputField.stopLossLimitPrice.rawValue) + } else { + null + } + } else if (type == OrderType.TakeProfitLimit || type == OrderType.TakeProfitMarket) { + if (triggerPrice != null && triggerPrice <= 0) { + listOfNotNull(inputField) + } else if (limitPrice != null && limitPrice <= 0) { + listOf(TriggerOrdersInputField.takeProfitLimitPrice.rawValue) + } else { + null + } + } else { + null + } + + if (triggerPrice != null && triggerPrice <= 0 || (limitPrice != null && limitPrice <= 0)) { + return listOf( + error( + type = ErrorType.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", + ), + ) + } + + return null + } + + private fun validateCalculatedPricesPositiveDeprecated( triggerOrder: Map, ): List? { val type = parser.asString(triggerOrder["type"])?.let { @@ -314,6 +557,51 @@ internal class TriggerOrdersInputValidator( } private fun validateTriggerPrice( + triggerOrder: InternalTriggerOrderState, + oraclePrice: Double, + tickSize: String, + ): List? { + val type = triggerOrder.type ?: return null + val side = triggerOrder.side ?: return null + val triggerPrice = triggerOrder.price?.triggerPrice ?: return null + val inputField = triggerOrder.price?.input + + when (val triggerToIndex = requiredTriggerToIndexPrice(type, side)) { + RelativeToPrice.ABOVE -> { + if (triggerPrice <= oraclePrice) { + return listOf( + triggerToIndexError( + triggerToIndex = triggerToIndex, + oraclePrice = oraclePrice, + tickSize = tickSize, + type = type, + inputField = inputField, + ), + ) + } + } + + RelativeToPrice.BELOW -> { + if (triggerPrice >= oraclePrice) { + return listOf( + triggerToIndexError( + triggerToIndex = triggerToIndex, + oraclePrice = oraclePrice, + tickSize = tickSize, + type = type, + inputField = inputField, + ), + ) + } + } + + else -> {} + } + + return null + } + + private fun validateTriggerPriceDeprecated( triggerOrder: Map, oraclePrice: Double, tickSize: String, @@ -337,7 +625,7 @@ internal class TriggerOrdersInputValidator( RelativeToPrice.ABOVE -> { if (triggerPrice <= oraclePrice) { return listOf( - triggerToIndexError( + triggerToIndexErrorDeprecated( triggerToIndex, oraclePrice, tickSize, @@ -351,7 +639,7 @@ internal class TriggerOrdersInputValidator( RelativeToPrice.BELOW -> { if (triggerPrice >= oraclePrice) { return listOf( - triggerToIndexError( + triggerToIndexErrorDeprecated( triggerToIndex, oraclePrice, tickSize, @@ -368,6 +656,62 @@ internal class TriggerOrdersInputValidator( } private fun validateLimitPrice( + triggerOrder: InternalTriggerOrderState + ): List? { + val type = triggerOrder.type ?: return null + val side = triggerOrder.side ?: return null + + val fields = when (type) { + OrderType.StopLimit -> listOf(TriggerOrdersInputField.stopLossLimitPrice.rawValue) + OrderType.TakeProfitLimit -> listOf(TriggerOrdersInputField.takeProfitLimitPrice.rawValue) + else -> null + } + + when (type) { + OrderType.StopLimit, OrderType.TakeProfitLimit -> { + val limitPrice = triggerOrder.price?.limitPrice ?: return null + val triggerPrice = triggerOrder.price?.triggerPrice ?: return null + + if (side == OrderSide.Buy && limitPrice < triggerPrice) { + return limitPriceError( + errorCode = "LIMIT_MUST_ABOVE_TRIGGER_PRICE", + fields = fields, + titleStringKey = if (type == OrderType.StopLimit) { + "ERRORS.TRIGGERS_FORM_TITLE.STOP_LOSS_LIMIT_MUST_ABOVE_TRIGGER_PRICE" + } else { + "ERRORS.TRIGGERS_FORM_TITLE.TAKE_PROFIT_LIMIT_MUST_ABOVE_TRIGGER_PRICE" + }, + textStringKey = if (type == OrderType.StopLimit) { + "ERRORS.TRIGGERS_FORM.STOP_LOSS_LIMIT_MUST_ABOVE_TRIGGER_PRICE" + } else { + "ERRORS.TRIGGERS_FORM.TAKE_PROFIT_LIMIT_MUST_ABOVE_TRIGGER_PRICE" + }, + ) + } else if (side == OrderSide.Sell && limitPrice > triggerPrice) { + return limitPriceError( + errorCode = "LIMIT_MUST_BELOW_TRIGGER_PRICE", + fields = fields, + titleStringKey = if (type == OrderType.StopLimit) { + "ERRORS.TRIGGERS_FORM_TITLE.STOP_LOSS_LIMIT_MUST_BELOW_TRIGGER_PRICE" + } else { + "ERRORS.TRIGGERS_FORM_TITLE.TAKE_PROFIT_LIMIT_MUST_BELOW_TRIGGER_PRICE" + }, + textStringKey = if (type == OrderType.StopLimit) { + "ERRORS.TRIGGERS_FORM.STOP_LOSS_LIMIT_MUST_BELOW_TRIGGER_PRICE" + } else { + "ERRORS.TRIGGERS_FORM.TAKE_PROFIT_LIMIT_MUST_BELOW_TRIGGER_PRICE" + }, + ) + } + } + + else -> {} + } + + return null + } + + private fun validateLimitPriceDeprecated( triggerOrder: Map, ): List? { val type = parser.asString(triggerOrder["type"])?.let { OrderType.invoke(it) } @@ -390,7 +734,7 @@ internal class TriggerOrdersInputValidator( parser.asDouble(parser.value(triggerOrder, "price.triggerPrice")) ?.let { triggerPrice -> if (side == OrderSide.Buy && limitPrice < triggerPrice) { - return limitPriceError( + return limitPriceErrorDeprecated( "LIMIT_MUST_ABOVE_TRIGGER_PRICE", fields, if (type == OrderType.StopLimit) { @@ -405,7 +749,7 @@ internal class TriggerOrdersInputValidator( }, ) } else if (side == OrderSide.Sell && limitPrice > triggerPrice) { - return limitPriceError( + return limitPriceErrorDeprecated( "LIMIT_MUST_BELOW_TRIGGER_PRICE", fields, if (type == OrderType.StopLimit) { @@ -435,6 +779,24 @@ internal class TriggerOrdersInputValidator( fields: List?, titleStringKey: String, textStringKey: String, + ): List { + return listOf( + error( + type = ErrorType.error, + errorCode = errorCode, + fields = fields, + actionStringKey = "APP.TRADE.MODIFY_TRIGGER_PRICE", + titleStringKey = titleStringKey, + textStringKey = textStringKey, + ), + ) + } + + private fun limitPriceErrorDeprecated( + errorCode: String, + fields: List?, + titleStringKey: String, + textStringKey: String, ): List? { return listOf( errorDeprecated( @@ -449,6 +811,39 @@ internal class TriggerOrdersInputValidator( } private fun validateSize( + orderSize: Double?, + market: InternalMarketState?, + ): List? { + val symbol = market?.perpetualMarket?.assetId ?: return null + val orderSize = orderSize ?: return null + val minOrderSize = market.perpetualMarket?.configs?.minOrderSize ?: return null + + if (orderSize.abs() < minOrderSize) { + return listOf( + error( + type = ErrorType.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", + ), + "SYMBOL" to mapOf( + "value" to symbol, + "format" to "string", + ), + ), + ), + ) + } + return null + } + + private fun validateSizeDeprecated( orderSize: Double?, market: Map?, ): List? { @@ -494,6 +889,64 @@ internal class TriggerOrdersInputValidator( tickSize: String, type: OrderType, inputField: String?, + ): ValidationError { + val action = "APP.TRADE.MODIFY_TRIGGER_PRICE" + val params = mapOf( + "INDEX_PRICE" to + mapOf( + "value" to oraclePrice, + "format" to "price", + "tickSize" to tickSize, + ), + ) + val fields = listOfNotNull(inputField) + val isStopLoss = type == OrderType.StopLimit || type == OrderType.StopMarket + + return when (triggerToIndex) { + RelativeToPrice.ABOVE -> error( + type = ErrorType.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" + }, + textStringKey = if (isStopLoss) { + "ERRORS.TRIGGERS_FORM.STOP_LOSS_TRIGGER_MUST_ABOVE_INDEX_PRICE" + } else { + "ERRORS.TRIGGERS_FORM.TAKE_PROFIT_TRIGGER_MUST_ABOVE_INDEX_PRICE" + }, + textParams = params, + ) + + else -> error( + type = ErrorType.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" + }, + textStringKey = if (isStopLoss) { + "ERRORS.TRIGGERS_FORM.STOP_LOSS_TRIGGER_MUST_BELOW_INDEX_PRICE" + } else { + "ERRORS.TRIGGERS_FORM.TAKE_PROFIT_TRIGGER_MUST_BELOW_INDEX_PRICE" + }, + textParams = params, + ) + } + } + + private fun triggerToIndexErrorDeprecated( + triggerToIndex: RelativeToPrice, + oraclePrice: Double, + tickSize: String, + type: OrderType, + inputField: String?, ): Map { val action = "APP.TRADE.MODIFY_TRIGGER_PRICE" val params = mapOf( diff --git a/v4_abacus.podspec b/v4_abacus.podspec index 8ebcae8ce..811411fb5 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.3' + spec.version = '1.9.4' spec.homepage = 'https://github.com/dydxprotocol/v4-abacus' spec.source = { :http=> ''} spec.authors = ''