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 f945c3382..932108ced 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 @@ -425,7 +425,10 @@ 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/TradeInput.kt b/src/commonMain/kotlin/exchange.dydx.abacus/output/input/TradeInput.kt index f6a969d8f..ff74c9291 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?, @@ -735,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 } } } @@ -837,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/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/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 88c69ed02..72449648c 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, @@ -73,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, @@ -179,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/model/TradingStateMachine+TradeInput.kt b/src/commonMain/kotlin/exchange.dydx.abacus/state/model/TradingStateMachine+TradeInput.kt index c60eb1dd6..7fa839c8b 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 @@ -55,7 +55,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..d296ca4c5 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,29 +1526,35 @@ 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, - ) - this.input?.let { - input = Input.create( - existing = input, - parser = parser, - data = it, - environment = environment, + if (staticTyping) { + inputValidator.validate( internalState = internalState, - staticTyping = staticTyping, + 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, ) } + + 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/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/AccountInputValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/AccountInputValidator.kt index 2a670082b..25251765d 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? { + return null + } + + 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..282b8d342 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(textStringKey, 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..396f7a9a5 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? { + return null + } + + 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..22137cd57 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/TradeInputValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/TradeInputValidator.kt @@ -1,9 +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 @@ -12,15 +17,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 +36,41 @@ 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.TRADE && 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, + subaccountNumber = subaccountNumber ?: 0, + change = change, + restricted = restricted, + environment = environment, + ) + if (validatorErrors != null) { + errors.addAll(validatorErrors) + } + } + + return errors + } + + override fun validateDeprecated( wallet: Map?, user: Map?, subaccount: Map?, @@ -44,28 +83,14 @@ 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() - 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, @@ -83,18 +108,14 @@ internal class TradeInputValidator( return null } - private fun change( - parser: ParserProtocol, - subaccount: Map?, - trade: Map, + private fun getPositionChange( + subaccount: InternalSubaccountState?, + trade: InternalTradeInputState, ): PositionChange { - val marketId = parser.asString(trade["marketId"]) ?: return PositionChange.NONE - val position = - parser.asNativeMap(parser.value(subaccount, "openPositions.$marketId")) - ?: return PositionChange.NONE - val size = parser.asDouble(parser.value(position, "size.current")) ?: Numeric.double.ZERO - val postOrder = - parser.asDouble(parser.value(position, "size.postOrder")) ?: Numeric.double.ZERO + 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) { @@ -130,70 +151,50 @@ internal class TradeInputValidator( } } - private fun validateClosingOnly( + private fun getPositionChangeDeprecated( 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 + ): PositionChange { + val marketId = parser.asString(trade["marketId"]) ?: return PositionChange.NONE + val position = + parser.asNativeMap(parser.value(subaccount, "openPositions.$marketId")) + ?: return PositionChange.NONE + val size = parser.asDouble(parser.value(position, "size.current")) ?: Numeric.double.ZERO + val postOrder = + parser.asDouble(parser.value(position, "size.postOrder")) ?: 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 { - 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 + PositionChange.CLOSING } } 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", - ), - ), - ) + if (postOrder != Numeric.double.ZERO) { + PositionChange.NEW + } else { + PositionChange.NONE + } } } } diff --git a/src/commonMain/kotlin/exchange.dydx.abacus/validator/TransferInputValidator.kt b/src/commonMain/kotlin/exchange.dydx.abacus/validator/TransferInputValidator.kt index 3791f0993..0cfd1db3b 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? { + return null + } + + 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..38bd13989 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? { + return null + } + + 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..780cdeb77 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,14 @@ internal interface ValidatorProtocol { internal interface TradeValidatorProtocol { fun validateTrade( - staticTyping: Boolean, internalState: InternalState, + subaccountNumber: Int?, + change: PositionChange, + restricted: Boolean, + environment: V4Environment?, + ): List? + + fun validateTradeDeprecated( subaccount: Map?, market: Map?, configs: Map?, @@ -54,8 +68,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..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,9 +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 @@ -15,11 +25,66 @@ 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, + subaccountNumber: Int?, + change: PositionChange, + restricted: Boolean, + environment: V4Environment? + ): List? { + 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( subaccount: Map?, market: Map?, configs: Map?, @@ -36,7 +101,7 @@ internal class TradeAccountStateValidator( when (isIsolatedMarginTrade) { true -> { - val crossOrdersError = validateSubaccountCrossOrders( + val crossOrdersError = validateSubaccountCrossOrdersDeprecated( parser, subaccount, trade, @@ -44,7 +109,7 @@ internal class TradeAccountStateValidator( if (crossOrdersError != null) { errors.add(crossOrdersError) } - val postAllOrdersError = validateSubaccountPostOrders( + val postAllOrdersError = validateSubaccountPostOrdersDeprecated( parser, subaccount, trade, @@ -55,7 +120,7 @@ internal class TradeAccountStateValidator( } } false -> { - val marginError = validateSubaccountMarginUsage( + val marginError = validateSubaccountMarginUsageDeprecated( parser, subaccount, change, @@ -63,7 +128,7 @@ internal class TradeAccountStateValidator( if (marginError != null) { errors.add(marginError) } - val crossOrdersError = validateSubaccountCrossOrders( + val crossOrdersError = validateSubaccountCrossOrdersDeprecated( parser, subaccount, trade, @@ -71,7 +136,7 @@ internal class TradeAccountStateValidator( if (crossOrdersError != null) { errors.add(crossOrdersError) } - val postAllOrdersError = validateSubaccountPostOrders( + val postAllOrdersError = validateSubaccountPostOrdersDeprecated( parser, subaccount, trade, @@ -90,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, @@ -110,13 +210,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 @@ -126,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, @@ -133,19 +258,19 @@ internal class TradeAccountStateValidator( /* ORDER_CROSSES_OWN_ORDER */ - return if (fillsExistingOrder( + return if (fillsExistingOrderDeprecated( parser, trade, 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 @@ -153,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?, @@ -222,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, @@ -230,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"]) @@ -238,19 +459,19 @@ 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 } } 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 @@ -262,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?, @@ -272,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 6134ca1d6..6a31afdbc 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeBracketOrdersValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeBracketOrdersValidator.kt @@ -1,9 +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 @@ -14,11 +20,34 @@ 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, + subaccountNumber: Int?, + change: PositionChange, + restricted: Boolean, + environment: V4Environment? + ): List? { + 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( subaccount: Map?, market: Map?, configs: Map?, @@ -33,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, @@ -45,13 +74,42 @@ 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) + } + + validateStopLoss( + 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, @@ -60,7 +118,7 @@ internal class TradeBracketOrdersValidator( if (takeProfitError != null) { errors.add(takeProfitError) } - val stopError = validateStopLoss( + val stopError = validateStopLossDeprecated( position, trade, price, @@ -73,6 +131,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, @@ -80,17 +161,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, @@ -99,7 +229,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", @@ -119,7 +249,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", @@ -142,6 +272,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, @@ -155,7 +338,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", @@ -178,7 +361,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", @@ -204,6 +387,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? { @@ -215,7 +431,7 @@ internal class TradeBracketOrdersValidator( when (parser.asString(trade["side"])) { "SELL" -> { if (sizePostOrder > 0.0) { - reduceOnlyError( + reduceOnlyErrorDeprecated( listOf("brackets.takeProfit.triggerPrice", "brackets.takeProfit.reduceOnly"), ) } else { @@ -225,7 +441,7 @@ internal class TradeBracketOrdersValidator( "BUY" -> { if (sizePostOrder < 0.0) { - reduceOnlyError( + reduceOnlyErrorDeprecated( listOf("brackets.takeProfit.triggerPrice", "brackets.takeProfit.reduceOnly"), ) } else { @@ -241,6 +457,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, @@ -248,17 +487,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, @@ -267,7 +555,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", @@ -287,7 +575,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", @@ -310,6 +598,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, @@ -323,7 +664,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", @@ -343,7 +684,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", @@ -366,6 +707,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? { @@ -376,7 +750,7 @@ internal class TradeBracketOrdersValidator( when (parser.asString(trade["side"])) { "SELL" -> { if (sizePostOrder > 0.0) { - reduceOnlyError( + reduceOnlyErrorDeprecated( listOf("brackets.stopLoss.triggerPrice", "brackets.stopLoss.reduceOnly"), ) } else { @@ -386,7 +760,7 @@ internal class TradeBracketOrdersValidator( "BUY" -> { if (sizePostOrder < 0.0) { - reduceOnlyError( + reduceOnlyErrorDeprecated( listOf("brackets.stopLoss.triggerPrice", "brackets.stopLoss.reduceOnly"), ) } else { @@ -401,34 +775,65 @@ internal class TradeBracketOrdersValidator( } } - private fun triggerPriceError( + private fun triggerPriceErrorDeprecated( errorCode: String, fields: List, title: String, text: String, params: Map?, ): Map { + return errorDeprecated( + type = "ERROR", + errorCode = errorCode, + fields = fields, + actionStringKey = "APP.TRADE.ENTER_TRIGGER_PRICE", + titleStringKey = title, + textStringKey = text, + textParams = params, + ) + } + + private fun triggerPriceError( + errorCode: String, + fields: List, + title: String, + text: String, + params: Map?, + ): ValidationError { return error( - "ERROR", - errorCode, - fields, - "APP.TRADE.ENTER_TRIGGER_PRICE", - title, - text, - params, + type = ErrorType.error, + errorCode = errorCode, + fields = fields, + actionStringKey = "APP.TRADE.ENTER_TRIGGER_PRICE", + titleStringKey = title, + textStringKey = text, + textParams = params, ) } - private fun reduceOnlyError( + private fun reduceOnlyErrorDeprecated( field: List, ): Map { + 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", + ) + } + + private fun reduceOnlyError( + field: List, + ): ValidationError { 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", + 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 361ab0192..68db12cf3 100644 --- a/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeInputDataValidator.kt +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeInputDataValidator.kt @@ -1,10 +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 @@ -31,12 +37,43 @@ 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, + subaccountNumber: Int?, + change: PositionChange, + restricted: Boolean, + environment: V4Environment? + ): List? { + 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( subaccount: Map?, market: Map?, configs: Map?, @@ -53,13 +90,13 @@ internal class TradeInputDataValidator( trade: Map, ): List? { val errors = mutableListOf() - validateSize(trade, market)?.let { + validateSizeDeprecate(trade, market)?.let { /* ORDER_SIZE_BELOW_MIN_SIZE */ errors.addAll(it) } - validateLimitPrice(trade, market)?.let { + validateLimitPriceDeprecated(trade, market)?.let { /* LIMIT_MUST_ABOVE_TRIGGER_PRICE LIMIT_MUST_BELOW_TRIGGER_PRICE @@ -69,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 @@ -83,6 +120,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? { @@ -96,14 +169,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", @@ -124,6 +197,54 @@ internal class TradeInputDataValidator( } private fun validateLimitPrice( + trade: InternalTradeInputState, + ): List? { + /* + LIMIT_MUST_ABOVE_TRIGGER_PRICE + LIMIT_MUST_BELOW_TRIGGER_PRICE + */ + return when (trade.type) { + OrderType.StopLimit, 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 validateLimitPriceDeprecated( trade: Map, market: Map?, ): List? { @@ -143,25 +264,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 { @@ -179,6 +300,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 validateTimeInForceDeprecated( trade: Map, market: Map?, ): List? { @@ -200,13 +343,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..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,10 +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 @@ -19,8 +23,28 @@ internal class TradeMarketOrderInputValidator( private val marketOrderWarningSlippage = 0.05 override fun validateTrade( - staticTyping: Boolean, internalState: InternalState, + subaccountNumber: Int?, + change: PositionChange, + restricted: Boolean, + environment: V4Environment? + ): List? { + 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( subaccount: Map?, market: Map?, configs: Map?, @@ -67,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 @@ -77,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"), @@ -89,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"), @@ -99,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 @@ -126,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", @@ -142,21 +241,46 @@ internal class TradeMarketOrderInputValidator( } } - private fun createTradeBoxWarningOrError( + private fun createTradeBoxWarningOrErrorDeprecated( errorLevel: String, errorCode: String, fields: List? = null, actionStringKey: String? = null, slippagePercentValue: Double? = null ): Map { + return errorDeprecated( + 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", + ), + ) + }, + ) + } + + private fun createTradeBoxWarningOrError( + errorLevel: ErrorType, + errorCode: String, + fields: List? = null, + actionStringKey: String? = null, + slippagePercentValue: Double? = null + ): ValidationError { return error( 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..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,9 +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 @@ -14,11 +20,37 @@ 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, + subaccountNumber: Int?, + change: PositionChange, + restricted: Boolean, + environment: V4Environment? + ): List? { + 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( subaccount: Map?, market: Map?, configs: Map?, @@ -40,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, ) @@ -63,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 - } + 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( - 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( - "MARKET" to mapOf( - "value" to marketId, + 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( + "MAX_SIZE" to mapOf( + "value" to maxSize, + "format" to "size", + ), + "SYMBOL" to mapOf( + "value" to symbol, "format" to "string", ), ), @@ -98,7 +128,7 @@ internal class TradePositionStateValidator( } } - private fun validatePositionSize( + private fun validatePositionSizeDeprecated( position: Map?, market: Map?, ): Map? { @@ -113,14 +143,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", @@ -137,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? { @@ -146,13 +202,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..c097c0cc8 --- /dev/null +++ b/src/commonMain/kotlin/exchange.dydx.abacus/validator/trade/TradeResctrictedValidator.kt @@ -0,0 +1,188 @@ +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 +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, + subaccountNumber: Int?, + change: PositionChange, + restricted: Boolean, + environment: V4Environment? + ): List? { + 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( + 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 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?, + 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.canReduce")) ?: 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..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,9 +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 @@ -15,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 } } } @@ -24,6 +31,105 @@ internal class TradeTriggerPriceValidator( 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 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 + } + /* They are still used to calculate payload, but no longer used for validation private val stopMarketSlippageBufferBTC = 0.05; // 5% for Stop Market @@ -32,9 +138,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?, @@ -58,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 @@ -66,7 +170,7 @@ internal class TradeTriggerPriceValidator( RelativeToPrice.ABOVE -> { if (triggerPrice <= oraclePrice) { errors.add( - triggerToIndexError( + triggerToIndexErrorDeprecated( triggerToIndex, type, oraclePrice, @@ -79,7 +183,7 @@ internal class TradeTriggerPriceValidator( RelativeToPrice.BELOW -> { if (triggerPrice >= oraclePrice) { errors.add( - triggerToIndexError( + triggerToIndexErrorDeprecated( triggerToIndex, type, oraclePrice, @@ -91,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, @@ -115,7 +219,7 @@ internal class TradeTriggerPriceValidator( RelativeToPrice.BELOW -> { if (triggerPrice >= liquidationPrice) { errors.add( - triggerToLiquidationError( + triggerToLiquidationErrorDeprecated( triggerToLiquidation, liquidationPrice, tickSize, @@ -131,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) { @@ -152,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, @@ -178,29 +350,52 @@ 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, ) } } 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, @@ -224,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? { @@ -240,7 +444,7 @@ internal class TradeTriggerPriceValidator( triggerToLiquidation: RelativeToPrice, triggerLiquidation: Double, tickSize: String, - ): Map { + ): ValidationError { val fields = listOf("price.triggerPrice") val action = "APP.TRADE.MODIFY_TRIGGER_PRICE" // Localizations uses TRIGGER_PRICE_LIMIT as paramater name @@ -254,23 +458,62 @@ 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, + 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( - "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, + 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" + // 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 -> 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 -> 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..bcf5b838d 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? { + return null + } + + 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,