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-469: Add basic order lines to market chart #90

Merged
merged 2 commits into from
May 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.hilt.navigation.compose.hiltViewModel
import com.github.mikephil.charting.charts.CombinedChart
import com.github.mikephil.charting.components.LimitLine
import com.github.mikephil.charting.data.BarDataSet
import com.github.mikephil.charting.data.BarEntry
import com.github.mikephil.charting.data.CandleEntry
Expand Down Expand Up @@ -80,6 +80,7 @@ object DydxMarketPricesView : DydxComponent {
val candles: CandleChartDataSet?,
val volumes: BarDataSet?,
val prices: LineChartDataSet?,
val orderLines: List<LimitLine>,
val typeOptions: SelectionOptions,
val resolutionOptions: SelectionOptions,
val highlight: PriceHighlight? = null,
Expand Down Expand Up @@ -113,6 +114,9 @@ object DydxMarketPricesView : DydxComponent {
),
"funding",
),
listOf(
LimitLine(1f),
),
typeOptions = SelectionOptions(
titles = listOf("Candles", "Lines"),
index = 0,
Expand All @@ -131,7 +135,7 @@ object DydxMarketPricesView : DydxComponent {
override fun Content(modifier: Modifier) {
val viewModel: DydxMarketPricesViewModel = hiltViewModel()

val state = viewModel.state.collectAsStateWithLifecycle(initialValue = null).value
val state = viewModel.state.collectAsStateWithLifecycle().value
Content(modifier, state)
}

Expand Down Expand Up @@ -341,47 +345,44 @@ object DydxMarketPricesView : DydxComponent {

@Composable
private fun ChartContent(modifier: Modifier, state: ViewState) {
val context = LocalContext.current
// Create a reference to the regular Android View

val chart = remember {
CombinedChart(context).apply {
config(state.config)
}
}

chart.update(
if (state.typeOptions.index == 0) state.candles else null,
state.volumes,
if (state.typeOptions.index == 0) null else state.prices,
state.config,
null,
) {
if (market != state.market) {
// Show 40 items
chart.data.barData.dataSets.firstOrNull()?.xMax?.let {
chart.setVisibleXRange(40f, 40f)
chart.moveViewToX(it)
// The minXRange has a higher number than maxXRange
// because the minXRange is the range for minXScale
// and the maxXRange is the range for maxXScale
// and range and scale are inverse
chart.setVisibleXRange(160f, 40f)
}
market = state.market
}
}

Column(
modifier = modifier
.fillMaxWidth()
.fillMaxHeight(),
) {
AndroidView(
factory = { chart },
factory = { context ->
CombinedChart(context).apply {
config(state.config)
}
},
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(),
update = { chart ->
chart.update(
if (state.typeOptions.index == 0) state.candles else null,
state.volumes,
if (state.typeOptions.index == 0) null else state.prices,
state.orderLines,
state.config,
null,
) { lastX ->
if (market != state.market) {
// Show 40 items
chart.data.barData.dataSets.firstOrNull()?.xMax?.let {
chart.setVisibleXRange(40f, 40f)
chart.moveViewToX(lastX)
// The minXRange has a higher number than maxXRange
// because the minXRange is the range for minXScale
// and the maxXRange is the range for maxXScale
// and range and scale are inverse
chart.setVisibleXRange(160f, 40f)
}
market = state.market
}
}
},
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package exchange.dydx.trading.feature.market.marketinfo.components.prices

import androidx.compose.ui.graphics.toArgb
import androidx.lifecycle.ViewModel
import com.github.mikephil.charting.components.LimitLine
import com.github.mikephil.charting.data.BarDataSet
import com.github.mikephil.charting.data.BarEntry
import com.github.mikephil.charting.data.CandleDataSet
Expand All @@ -13,6 +14,8 @@ import com.hoc081098.flowext.combine
import dagger.hilt.android.lifecycle.HiltViewModel
import exchange.dydx.abacus.output.MarketCandle
import exchange.dydx.abacus.output.PerpetualMarket
import exchange.dydx.abacus.output.input.OrderSide
import exchange.dydx.abacus.output.input.OrderStatus
import exchange.dydx.abacus.protocols.LocalizerProtocol
import exchange.dydx.dydxstatemanager.AbacusStateManagerProtocol
import exchange.dydx.platformui.components.charts.config.AxisConfig
Expand All @@ -25,17 +28,24 @@ import exchange.dydx.platformui.components.charts.config.InteractionConfig
import exchange.dydx.platformui.components.charts.config.LabelConfig
import exchange.dydx.platformui.components.charts.config.LineChartDrawingConfig
import exchange.dydx.platformui.components.charts.view.LineChartDataSet
import exchange.dydx.platformui.designSystem.theme.ThemeColor
import exchange.dydx.platformui.designSystem.theme.ThemeColor.SemanticColor
import exchange.dydx.platformui.designSystem.theme.color
import exchange.dydx.platformui.designSystem.theme.negativeColor
import exchange.dydx.platformui.designSystem.theme.positiveColor
import exchange.dydx.trading.common.DydxViewModel
import exchange.dydx.trading.common.di.CoroutineScopes
import exchange.dydx.trading.common.formatter.DydxFormatter
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import java.time.Instant
import java.time.temporal.ChronoUnit
import javax.inject.Inject
Expand All @@ -45,6 +55,7 @@ class DydxMarketPricesViewModel @Inject constructor(
private val localizer: LocalizerProtocol,
private val abacusStateManager: AbacusStateManagerProtocol,
private val formatter: DydxFormatter,
@CoroutineScopes.ViewModel private val viewModelScope: CoroutineScope,
) : ViewModel(), DydxViewModel, OnChartValueSelectedListener {
/*
The library works well with x range up to 1000
Expand Down Expand Up @@ -79,44 +90,55 @@ class DydxMarketPricesViewModel @Inject constructor(
private val resolutionIndex = MutableStateFlow(candlesPeriods.indexOf(abacusStateManager.candlesPeriod.value))
private val selectedPrice = MutableStateFlow<MarketCandle?>(null)

val offset = 900
private val offset = 900

val anchorDateTime: Instant = run {
private val anchorDateTime: Instant = run {
val now = Instant.now()
now.truncatedTo(ChronoUnit.DAYS)
now
}

val state: Flow<DydxMarketPricesView.ViewState?> =
@OptIn(ExperimentalCoroutinesApi::class)
val state: StateFlow<DydxMarketPricesView.ViewState?> =
combine(
abacusStateManager.state.tradeInput.map { it?.marketId },
abacusStateManager.state.marketMap,
abacusStateManager.state.candlesMap,
abacusStateManager.state.tradeInput.map { it?.marketId }
.filterNotNull()
.flatMapLatest { abacusStateManager.state.selectedSubaccountOrdersOfMarket(it) },
prashanDYDX marked this conversation as resolved.
Show resolved Hide resolved
selectedPrice,
typeIndex,
resolutionIndex,
) { marketId, marketMap, candlesMap, selectedPrice, typeIndex, resolutionIndex ->
) { marketId, marketMap, candlesMap, ordersForMarket, selectedPrice, typeIndex, resolutionIndex ->
if (marketId == null) {
return@combine null
}
val market = marketMap?.get(marketId)
val allPrices = candlesMap?.get(marketId)
val candlesPeriod = candlesPeriods[resolutionIndex]
val prices = allPrices?.candles?.get(candlesPeriod)
val orderData = ordersForMarket?.run {
filter { it.status in listOf(OrderStatus.open, OrderStatus.untriggered, OrderStatus.partiallyFilled) }
.map { OrderData(it.price, it.side) }
}.orEmpty()
createViewState(
prices,
market,
candlesPeriod,
selectedPrice,
typeIndex,
resolutionIndex,
prices = prices,
market = market,
orderData = orderData,
candlesPeriod = candlesPeriod,
selectedPrice = selectedPrice,
typeIndex = typeIndex,
resolutionIndex = resolutionIndex,
)
}
.distinctUntilChanged()
.stateIn(viewModelScope, SharingStarted.Lazily, null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any benefit of using StateFlow here? The state is already scoped as local var to the view model, so ".stateIn(viewModelScope,..)" seems like an overkill. The view is the only consumer, so vanilla Flow should be sufficient.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It allows us to avoid setting the initial value in Compose: state.collectAsState() vs state.collectAsState(initialValue = null. This is more testable, and allows us to test that our initial state is in fact null. (Even though we aren't writing tests today, we should be writing code in a testable way.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well.. if the initialState is declared as null by the View, then there is no need to test it's null.

It's fine to land this if it doesn't incur significant performance overhead.


private fun createViewState(
prices: List<MarketCandle>?,
market: PerpetualMarket?,
orderData: List<OrderData>,
candlesPeriod: String,
selectedPrice: MarketCandle?,
typeIndex: Int,
Expand Down Expand Up @@ -194,9 +216,19 @@ class DydxMarketPricesViewModel @Inject constructor(
localizer = localizer,
config = config(market, candlesPeriod),
market = market?.id,
CandleDataSet(candles, "candles"),
BarDataSet(volumes, "volumes"),
LineChartDataSet(lines, "lines"),
candles = CandleDataSet(candles, "candles"),
volumes = BarDataSet(volumes, "volumes"),
prices = LineChartDataSet(lines, "lines"),
orderLines = orderData.map { (price, side) ->
LimitLine(price.toFloat())
.apply {
lineColor = when (side) {
OrderSide.buy -> SemanticColor.positiveColor.color.toArgb()
OrderSide.sell -> SemanticColor.negativeColor.color.toArgb()
}
enableDashedLine(20f, 10f, 0f)
}
},
typeOptions = SelectionOptions(
typeTitles,
typeIndex,
Expand Down Expand Up @@ -279,17 +311,17 @@ class DydxMarketPricesViewModel @Inject constructor(
private fun config(market: PerpetualMarket?, candlesPeriod: String?): CombinedChartConfig {
return CombinedChartConfig(
candlesDrawing = CandlesDrawingConfig(
increasingColor = ThemeColor.SemanticColor.positiveColor.color.toArgb(),
decreasingColor = ThemeColor.SemanticColor.negativeColor.color.toArgb(),
neutralColor = exchange.dydx.platformui.designSystem.theme.ThemeColor.SemanticColor.text_primary.color.toArgb(),
increasingColor = SemanticColor.positiveColor.color.toArgb(),
decreasingColor = SemanticColor.negativeColor.color.toArgb(),
neutralColor = SemanticColor.text_primary.color.toArgb(),
),
barDrawing = BarDrawingConfig(
borderColor = exchange.dydx.platformui.designSystem.theme.ThemeColor.SemanticColor.layer_6.color.toArgb(),
fillColor = exchange.dydx.platformui.designSystem.theme.ThemeColor.SemanticColor.layer_6.color.toArgb(),
borderColor = SemanticColor.layer_6.color.toArgb(),
fillColor = SemanticColor.layer_6.color.toArgb(),
),
lineDrawing = LineChartDrawingConfig(
2.0f,
exchange.dydx.platformui.designSystem.theme.ThemeColor.SemanticColor.text_secondary.color.toArgb(),
SemanticColor.text_secondary.color.toArgb(),
null,
),
drawing = DrawingConfig(
Expand All @@ -305,7 +337,7 @@ class DydxMarketPricesViewModel @Inject constructor(
LabelConfig(
DateTimeAxisFormatter(anchorDateTime, candlesPeriod, offset),
8.0f,
exchange.dydx.platformui.designSystem.theme.ThemeColor.SemanticColor.text_secondary.color.toArgb(),
SemanticColor.text_secondary.color.toArgb(),
AxisTextPosition.OUTSIDE,
),
),
Expand All @@ -315,7 +347,7 @@ class DydxMarketPricesViewModel @Inject constructor(
LabelConfig(
PriceAxisFormatter(formatter, market?.configs?.tickSizeDecimals ?: 4),
8.0f,
exchange.dydx.platformui.designSystem.theme.ThemeColor.SemanticColor.text_secondary.color.toArgb(),
SemanticColor.text_secondary.color.toArgb(),
AxisTextPosition.OUTSIDE,
),
),
Expand All @@ -335,3 +367,8 @@ class DydxMarketPricesViewModel @Inject constructor(
selectedPrice.value = null
}
}

private data class OrderData(
val price: Double,
val side: OrderSide
)
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ class AbacusState(
return selectedSubaccountOrders
.map { orders ->
orders?.filter { order ->
order?.marketId == marketId
order.marketId == marketId
}
}
.stateIn(stateManagerScope, SharingStarted.Lazily, null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package exchange.dydx.platformui.components.charts.view

import com.github.mikephil.charting.charts.BarLineChartBase
import com.github.mikephil.charting.charts.CombinedChart
import com.github.mikephil.charting.components.LimitLine
import com.github.mikephil.charting.data.BarData
import com.github.mikephil.charting.data.BarDataSet
import com.github.mikephil.charting.data.BarLineScatterCandleBubbleData
Expand Down Expand Up @@ -101,6 +102,7 @@ fun CombinedChart.update(
candles: CandleChartDataSet?,
bars: BarDataSet?,
line: LineChartDataSet?,
limits: List<LimitLine>,
config: ICombinedChartConfig,
lineColor: Int? = null,
updateRange: (lastX: Float) -> Unit = {}
Expand Down Expand Up @@ -150,6 +152,10 @@ fun CombinedChart.update(
}
}

limits.forEach {
this.axisLeft.addLimitLine(it)
}

notifyDataSetChanged()
invalidate()
}
Expand Down
Loading