Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MOB-441 add TP/SL display to market screen (Android) #71

Merged
merged 11 commits into from
Apr 22, 2024
Original file line number Diff line number Diff line change
@@ -1,22 +1,44 @@
package exchange.dydx.trading.feature.market.marketinfo.components.position

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import exchange.dydx.abacus.protocols.LocalizerProtocol
import exchange.dydx.platformui.components.buttons.PlatformButton
import exchange.dydx.platformui.components.buttons.PlatformButtonState
import exchange.dydx.platformui.components.dividers.PlatformDivider
import exchange.dydx.platformui.designSystem.theme.ThemeColor
import exchange.dydx.platformui.designSystem.theme.ThemeFont
import exchange.dydx.platformui.designSystem.theme.ThemeShapes
import exchange.dydx.platformui.designSystem.theme.dydxDefault
import exchange.dydx.platformui.designSystem.theme.themeColor
import exchange.dydx.platformui.designSystem.theme.themeFont
import exchange.dydx.trading.common.component.DydxComponent
import exchange.dydx.trading.common.compose.collectAsStateWithLifecycle
import exchange.dydx.trading.common.theme.DydxThemedPreviewSurface
import exchange.dydx.trading.common.theme.MockLocalizer
import exchange.dydx.trading.feature.shared.scarfolds.InputFieldScarfold
import exchange.dydx.utilities.utils.toDp

@Preview
@Composable
Expand All @@ -30,14 +52,35 @@ fun Preview_DydxMarketPositionButtonsView() {
}

object DydxMarketPositionButtonsView : DydxComponent {

data class TriggerViewState(
val label: String? = null,
val triggerPrice: String? = null,
val limitPrice: String? = null,
val sizePercent: String? = null,
) {
companion object {
val preview = TriggerViewState(
label = "TP",
triggerPrice = "$120.0",
limitPrice = "$110.0",
sizePercent = "10%",
)
}
}

data class ViewState(
val localizer: LocalizerProtocol,
val addTriggerAction: (() -> Unit)? = null,
val closeAction: (() -> Unit)? = null,
val takeProfitTrigger: TriggerViewState? = null,
val stopLossTrigger: TriggerViewState? = null,
) {
companion object {
val preview = ViewState(
localizer = MockLocalizer(),
takeProfitTrigger = TriggerViewState.preview,
stopLossTrigger = TriggerViewState.preview,
)
}
}
Expand All @@ -61,25 +104,166 @@ object DydxMarketPositionButtonsView : DydxComponent {
.padding(horizontal = ThemeShapes.HorizontalPadding),
horizontalArrangement = Arrangement.SpaceBetween,
) {
PlatformButton(
text = state.localizer.localize("APP.TRADE.ADD_TP_SL"),
state = PlatformButtonState.Secondary,
modifier = Modifier
.padding(vertical = ThemeShapes.VerticalPadding)
.weight(1f),
action = state.addTriggerAction ?: {},
)
var size by remember { mutableStateOf(IntSize.Zero) }

if (state.takeProfitTrigger == null && state.stopLossTrigger == null) {
prashanDYDX marked this conversation as resolved.
Show resolved Hide resolved
PlatformButton(
text = state.localizer.localize("APP.TRADE.ADD_TP_SL"),
prashanDYDX marked this conversation as resolved.
Show resolved Hide resolved
state = PlatformButtonState.Secondary,
modifier = Modifier
.padding(vertical = ThemeShapes.VerticalPadding)
.weight(1f),
action = state.addTriggerAction ?: {},
)

Spacer(modifier = Modifier.width(ThemeShapes.HorizontalPadding))
Spacer(modifier = Modifier.width(ThemeShapes.HorizontalPadding))

PlatformButton(
text = state.localizer.localize("APP.TRADE.CLOSE_POSITION"),
state = PlatformButtonState.Destructive,
PlatformButton(
text = state.localizer.localize("APP.TRADE.CLOSE_POSITION"),
state = PlatformButtonState.Destructive,
modifier = Modifier
.padding(vertical = ThemeShapes.VerticalPadding)
.weight(1f),
action = state.closeAction ?: {},
)
} else {
Column(
modifier = Modifier,
verticalArrangement = Arrangement.spacedBy(ThemeShapes.VerticalPadding),
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(ThemeShapes.HorizontalPadding),
) {
state.takeProfitTrigger?.let {
TriggerViewContent(
modifier = Modifier
.weight(1f)
.onSizeChanged { size = it },
state = it,
localizer = state.localizer,
action = state.addTriggerAction ?: {},
)
} ?: PlatformButton(
text = state.localizer.localize("APP.TRADE.ADD_TP"),
state = PlatformButtonState.Secondary,
modifier = Modifier
.padding(vertical = ThemeShapes.VerticalPadding)
.weight(1f)
.height(size.height.toDp),
action = state.addTriggerAction ?: {},
)

state.stopLossTrigger?.let {
TriggerViewContent(
modifier = Modifier
.weight(1f)
.onSizeChanged { size = it },
state = it,
localizer = state.localizer,
action = state.addTriggerAction ?: {},
)
} ?: PlatformButton(
text = state.localizer.localize("APP.TRADE.ADD_SL"),
state = PlatformButtonState.Secondary,
modifier = Modifier
.padding(vertical = ThemeShapes.VerticalPadding)
.weight(1f)
.height(size.height.toDp),
action = state.addTriggerAction ?: {},
)
}

PlatformButton(
text = state.localizer.localize("APP.TRADE.CLOSE_POSITION"),
state = PlatformButtonState.Destructive,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = ThemeShapes.VerticalPadding),
action = state.closeAction ?: {},
)
}
}
}
}

@Composable
private fun TriggerViewContent(
modifier: Modifier,
state: TriggerViewState,
localizer: LocalizerProtocol,
action: () -> Unit = {}
) {
InputFieldScarfold(
modifier = modifier,
) {
Column(
modifier = Modifier
.padding(vertical = ThemeShapes.VerticalPadding)
.weight(1f),
action = state.closeAction ?: {},
)
.clickable { action() }
.padding(8.dp),
verticalArrangement = Arrangement.spacedBy(ThemeShapes.VerticalPadding),
) {
Row(
modifier = Modifier,
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = state.label ?: "",
style = TextStyle.dydxDefault
.themeFont(fontSize = ThemeFont.FontSize.tiny)
.themeColor(ThemeColor.SemanticColor.text_tertiary),
)

Spacer(modifier = Modifier.weight(1f))

Text(
modifier = Modifier,
text = state.triggerPrice ?: "",
style = TextStyle.dydxDefault
.themeFont(fontSize = ThemeFont.FontSize.medium)
.themeColor(ThemeColor.SemanticColor.text_primary),
)
}

if (state.limitPrice != null || state.sizePercent != null) {
prashanDYDX marked this conversation as resolved.
Show resolved Hide resolved
PlatformDivider()

Row(
modifier = Modifier,
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = localizer.localize("APP.TRADE.LIMIT_ORDER_SHORT"),
style = TextStyle.dydxDefault
.themeFont(fontSize = ThemeFont.FontSize.tiny)
.themeColor(ThemeColor.SemanticColor.text_tertiary),
maxLines = 1,
modifier = Modifier.weight(1f),
)

if (state.limitPrice != null) {
Text(
text = state.limitPrice,
style = TextStyle.dydxDefault
.themeFont(fontSize = ThemeFont.FontSize.mini)
.themeColor(ThemeColor.SemanticColor.text_secondary),
)
}

if (state.sizePercent != null) {
Text(
text = state.sizePercent,
style = TextStyle.dydxDefault
.themeFont(fontSize = ThemeFont.FontSize.mini)
.themeColor(ThemeColor.SemanticColor.text_secondary),
)
}
}
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,49 +2,108 @@ package exchange.dydx.trading.feature.market.marketinfo.components.position

import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import exchange.dydx.abacus.output.SubaccountOrder
import exchange.dydx.abacus.output.SubaccountPosition
import exchange.dydx.abacus.protocols.LocalizerProtocol
import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol
import exchange.dydx.dydxstatemanager.MarketConfigsAndAsset
import exchange.dydx.dydxstatemanager.stopLossOrders
import exchange.dydx.dydxstatemanager.takeProfitOrders
import exchange.dydx.trading.common.DydxViewModel
import exchange.dydx.trading.common.formatter.DydxFormatter
import exchange.dydx.trading.common.navigation.DydxRouter
import exchange.dydx.trading.common.navigation.TradeRoutes
import exchange.dydx.trading.feature.market.marketinfo.streams.MarketAndAsset
import exchange.dydx.trading.feature.market.marketinfo.streams.MarketInfoStreaming
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.mapNotNull
import javax.inject.Inject

@HiltViewModel
class DydxMarketPositionButtonsViewModel @Inject constructor(
private val localizer: LocalizerProtocol,
marketInfoStream: MarketInfoStreaming,
private val router: DydxRouter,
private val abacusStateManager: AbacusStateManagerProtocol,
private val formatter: DydxFormatter,
) : ViewModel(), DydxViewModel {

private val marketIdFlow = marketInfoStream.marketAndAsset
.mapNotNull { it?.market?.id }

val state: Flow<DydxMarketPositionButtonsView.ViewState?> =
marketInfoStream.marketAndAsset.filterNotNull()
.map {
createViewState(it)
}
combine(
marketIdFlow,
marketIdFlow.flatMapLatest { abacusStateManager.state.selectedSubaccountPositionOfMarket(it) },
marketIdFlow.flatMapLatest { abacusStateManager.state.takeProfitOrders(it) },
marketIdFlow.flatMapLatest { abacusStateManager.state.stopLossOrders(it) },
abacusStateManager.state.configsAndAssetMap,
) { marketId, position, takeProfitOrders, stopLossOrders, configsAndAssetMap ->
createViewState(marketId, position, takeProfitOrders, stopLossOrders, configsAndAssetMap?.get(marketId))
}
.distinctUntilChanged()

private fun createViewState(
marketAndAsset: MarketAndAsset,
marketId: String,
position: SubaccountPosition?,
takeProfitOrders: List<SubaccountOrder>?,
stopLossOrders: List<SubaccountOrder>?,
configsAndAsset: MarketConfigsAndAsset?,
): DydxMarketPositionButtonsView.ViewState {
return DydxMarketPositionButtonsView.ViewState(
localizer = localizer,
addTriggerAction = {
router.navigateTo(
route = TradeRoutes.trigger + "/${marketAndAsset.market.id}",
route = TradeRoutes.trigger + "/$marketId",
presentation = DydxRouter.Presentation.Modal,
)
},
closeAction = {
router.navigateTo(
route = TradeRoutes.close_position + "/${marketAndAsset.market.id}",
route = TradeRoutes.close_position + "/$marketId",
presentation = DydxRouter.Presentation.Modal,
)
},
takeProfitTrigger = takeProfitOrders?.firstOrNull()?.let {
createTriggerViewState(
label = "TP",
position = position,
order = it,
configsAndAsset = configsAndAsset,
)
},
stopLossTrigger = stopLossOrders?.firstOrNull()?.let {
createTriggerViewState(
label = "SL",
position = position,
order = it,
configsAndAsset = configsAndAsset,
)
},
)
}

private fun createTriggerViewState(
label: String,
position: SubaccountPosition?,
order: SubaccountOrder,
configsAndAsset: MarketConfigsAndAsset?,
): DydxMarketPositionButtonsView.TriggerViewState {
val tickSize = configsAndAsset?.configs?.displayTickSizeDecimals ?: 0
val size = order.size
val positionSize = position?.size?.current ?: 0.0
val percentage = if (positionSize > 0.0) {
size / positionSize
} else {
0.0
}
return DydxMarketPositionButtonsView.TriggerViewState(
label = label,
triggerPrice = order.triggerPrice?.let { formatter.dollar(it, tickSize) },
limitPrice = order.price.let { formatter.dollar(it, tickSize) },
sizePercent = formatter.percent(percentage, 2),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ object DydxMarketPositionView : DydxComponent {
shareAction = {},
closeAction = {},
sharedMarketPositionViewState = SharedMarketPositionViewState.preview,
enableTrigger = true,
)
}
}
Expand Down
Loading
Loading