Skip to content
Closed
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
9 changes: 8 additions & 1 deletion app/schemas/one.mixin.android.db.PerpsDatabase/8.json
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@
},
{
"tableName": "markets",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`market_id` TEXT NOT NULL, `display_symbol` TEXT NOT NULL, `token_symbol` TEXT NOT NULL, `quote_symbol` TEXT NOT NULL, `mark_price` TEXT NOT NULL, `price_scale` INTEGER NOT NULL DEFAULT 2, `leverage` INTEGER NOT NULL, `icon_url` TEXT NOT NULL, `category` TEXT NOT NULL, `tags` TEXT NOT NULL, `funding_rate` TEXT NOT NULL, `funding_interval_hours` INTEGER NOT NULL DEFAULT 0, `next_funding_at` TEXT NOT NULL DEFAULT '', `open_interest` TEXT NOT NULL DEFAULT '0', `min_amount` TEXT NOT NULL, `max_amount` TEXT NOT NULL, `last` TEXT NOT NULL, `volume` TEXT NOT NULL, `high` TEXT NOT NULL, `low` TEXT NOT NULL, `open` TEXT NOT NULL, `change` TEXT NOT NULL, `bid_price` TEXT NOT NULL, `ask_price` TEXT NOT NULL, `created_at` TEXT NOT NULL, `updated_at` TEXT NOT NULL, `descriptions` TEXT, PRIMARY KEY(`market_id`))",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`market_id` TEXT NOT NULL, `display_symbol` TEXT NOT NULL, `token_symbol` TEXT NOT NULL, `quote_symbol` TEXT NOT NULL, `mark_price` TEXT NOT NULL, `price_scale` INTEGER NOT NULL DEFAULT 2, `leverage` INTEGER NOT NULL, `icon_url` TEXT NOT NULL, `category` TEXT NOT NULL, `tags` TEXT NOT NULL, `funding_rate` TEXT NOT NULL, `funding_interval_hours` INTEGER NOT NULL DEFAULT 0, `next_funding_at` TEXT NOT NULL DEFAULT '', `open_interest` TEXT NOT NULL DEFAULT '0', `min_amount` TEXT NOT NULL, `max_amount` TEXT NOT NULL, `last` TEXT NOT NULL, `volume` TEXT NOT NULL, `trade_volume_score_1d` INTEGER NOT NULL DEFAULT 0, `high` TEXT NOT NULL, `low` TEXT NOT NULL, `open` TEXT NOT NULL, `change` TEXT NOT NULL, `bid_price` TEXT NOT NULL, `ask_price` TEXT NOT NULL, `created_at` TEXT NOT NULL, `updated_at` TEXT NOT NULL, `descriptions` TEXT, PRIMARY KEY(`market_id`))",
"fields": [
{
"fieldPath": "marketId",
Expand Down Expand Up @@ -374,6 +374,13 @@
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tradeVolumeScore1D",
"columnName": "trade_volume_score_1d",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "high",
"columnName": "high",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,29 @@ class PerpsMigrationTest {
}
}

@Test
fun migrate_7_8_addsTradeVolumeScore() {
migrationTestHelper.createDatabase(Constants.DataBase.PERPS_DB_NAME, 7).apply {
insertMarket()
close()
}

val migratedDb =
migrationTestHelper.runMigrationsAndValidate(
Constants.DataBase.PERPS_DB_NAME,
8,
true,
PerpsDatabase.MIGRATION_7_8,
)

migratedDb.query(
"SELECT trade_volume_score_1d FROM markets WHERE market_id = 'market-1'",
).use { cursor ->
assertTrue(cursor.moveToFirst())
assertEquals(0, cursor.getInt(0))
}
}

private fun SupportSQLiteDatabase.insertMarket() {
execSQL(
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ data class PerpsMarket(
@ColumnInfo(name = "volume")
val volume: String,

@SerializedName("trade_volume_score_1d")
@ColumnInfo(name = "trade_volume_score_1d", defaultValue = "0")
val tradeVolumeScore1D: Int = 0,

@SerializedName("high")
@ColumnInfo(name = "high")
val high: String,
Expand Down
1 change: 1 addition & 0 deletions app/src/main/java/one/mixin/android/db/PerpsDatabase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ abstract class PerpsDatabase : RoomDatabase() {
db.execSQL("ALTER TABLE `markets` ADD COLUMN `funding_interval_hours` INTEGER NOT NULL DEFAULT 0")
db.execSQL("ALTER TABLE `markets` ADD COLUMN `next_funding_at` TEXT NOT NULL DEFAULT ''")
db.execSQL("ALTER TABLE `markets` ADD COLUMN `open_interest` TEXT NOT NULL DEFAULT '0'")
db.execSQL("ALTER TABLE `markets` ADD COLUMN `trade_volume_score_1d` INTEGER NOT NULL DEFAULT 0")
}
}

Expand Down
26 changes: 24 additions & 2 deletions app/src/main/java/one/mixin/android/db/perps/PerpsMarketDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import kotlinx.coroutines.flow.Flow
import one.mixin.android.api.response.perps.PerpsMarket
import one.mixin.android.db.BaseDao
Expand All @@ -16,12 +17,32 @@ interface PerpsMarketDao : BaseDao<PerpsMarket> {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(markets: List<PerpsMarket>)

@Query("SELECT * FROM markets WHERE market_id IN (:marketIds)")
suspend fun getMarkets(marketIds: List<String>): List<PerpsMarket>

@Transaction
suspend fun upsertPreservingTradeVolumeScores(markets: List<PerpsMarket>): List<PerpsMarket> {
if (markets.isEmpty()) return emptyList()
val existingScores =
getMarkets(markets.map(PerpsMarket::marketId))
.associate { market -> market.marketId to market.tradeVolumeScore1D }
val mergedMarkets =
markets.map { market ->
market.copy(
tradeVolumeScore1D = existingScores[market.marketId] ?: 0,
)
}
upsertList(mergedMarkets)
return mergedMarkets
}

@Query(
"""
SELECT *
FROM markets
WHERE CAST(volume AS REAL) > 0
ORDER BY CAST(volume AS REAL) DESC, token_symbol COLLATE NOCASE ASC, market_id ASC
ORDER BY trade_volume_score_1d DESC, CAST(volume AS REAL) DESC,
token_symbol COLLATE NOCASE ASC, market_id ASC
""",
)
suspend fun getAllMarkets(): List<PerpsMarket>
Expand All @@ -31,7 +52,8 @@ interface PerpsMarketDao : BaseDao<PerpsMarket> {
SELECT *
FROM markets
WHERE CAST(volume AS REAL) > 0
ORDER BY CAST(volume AS REAL) DESC, token_symbol COLLATE NOCASE ASC, market_id ASC
ORDER BY trade_volume_score_1d DESC, CAST(volume AS REAL) DESC,
token_symbol COLLATE NOCASE ASC, market_id ASC
""",
)
fun observeAllMarkets(): Flow<List<PerpsMarket>>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,34 +50,40 @@ class PerpsMarketRepository

suspend fun syncFavoriteMarkets(): List<PerpsMarket>? {
val markets = fetchMarkets(CATEGORY_FAVORITE) ?: return null
database.withTransaction {
marketDao.upsertList(markets)
return database.withTransaction {
val mergedMarkets = marketDao.upsertPreservingTradeVolumeScores(markets)
favoriteDao.replaceAll(
marketIds = markets.map(PerpsMarket::marketId),
marketIds = mergedMarkets.map(PerpsMarket::marketId),
createdAt = nowInUtc(),
)
mergedMarkets
}
return markets
}

suspend fun syncCategory(category: MarketCategory): List<PerpsMarket>? {
val markets = fetchMarkets(category.apiValue) ?: return null
database.withTransaction {
marketDao.upsertList(markets)
return database.withTransaction {
val mergedMarkets =
if (category == MarketCategory.TRENDING) {
marketDao.upsertList(markets)
markets
} else {
marketDao.upsertPreservingTradeVolumeScores(markets)
}
categoryDao.replaceCategory(
category = category.value,
marketIds = markets.map(PerpsMarket::marketId),
marketIds = mergedMarkets.map(PerpsMarket::marketId),
)
mergedMarkets
}
return markets
}

suspend fun refreshMarket(marketId: String): PerpsMarket? =
requestRouteAPI(
invokeNetwork = { routeService.getPerpsMarket(marketId) },
successBlock = { response ->
response.data?.withDefaults()?.also { market ->
marketDao.upsertSuspend(market)
response.data?.withDefaults()?.let { market ->
marketDao.upsertPreservingTradeVolumeScores(listOf(market)).single()
}
},
failureBlock = { true },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,5 @@ internal fun MarketPageUiState.analyticsSortField(column: MarketSortColumn): Str
MarketPriceChangePeriod.TWENTY_FOUR_HOURS -> AnalyticsTracker.MarketsSortField.TWENTY_FOUR_HOURS
MarketPriceChangePeriod.SEVEN_DAYS -> AnalyticsTracker.MarketsSortField.SEVEN_DAYS
}
MarketSortColumn.SCORE -> AnalyticsTracker.MarketsSortField.SCORE
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ enum class MarketSortColumn {
VOLUME,
PRICE,
CHANGE,
SCORE,
}

enum class MarketSortDirection {
Expand All @@ -65,14 +66,22 @@ data class MarketSortState(
val column: MarketSortColumn? = null,
val direction: MarketSortDirection = MarketSortDirection.DEFAULT,
) {
fun next(selectedColumn: MarketSortColumn): MarketSortState {
fun next(
selectedColumn: MarketSortColumn,
isScoreOrderingAvailable: Boolean = false,
): MarketSortState {
if (column != selectedColumn) {
return MarketSortState(selectedColumn, MarketSortDirection.DESCENDING)
}
return when (direction) {
MarketSortDirection.DEFAULT -> MarketSortState(selectedColumn, MarketSortDirection.DESCENDING)
MarketSortDirection.DESCENDING -> MarketSortState(selectedColumn, MarketSortDirection.ASCENDING)
MarketSortDirection.ASCENDING -> MarketSortState()
MarketSortDirection.ASCENDING ->
if (isScoreOrderingAvailable) {
scoreMarketSortState()
} else {
MarketSortState()
}
}
}
}
Expand All @@ -96,8 +105,7 @@ fun defaultMarketSortState(
MarketSortState(MarketSortColumn.VOLUME, MarketSortDirection.DESCENDING)
topTab == MarketTopTab.CRYPTO && subTab == MarketSubTab.TRENDING ->
MarketSortState(MarketSortColumn.VOLUME, MarketSortDirection.DESCENDING)
topTab == MarketTopTab.PERPETUAL && subTab == MarketSubTab.TRENDING ->
MarketSortState(MarketSortColumn.VOLUME, MarketSortDirection.DESCENDING)
isScoreOrderingAvailable(topTab, subTab) -> scoreMarketSortState()
topTab == MarketTopTab.PERPETUAL && subTab == MarketSubTab.MEME ->
MarketSortState(MarketSortColumn.VOLUME, MarketSortDirection.DESCENDING)
topTab == MarketTopTab.PERPETUAL && subTab == MarketSubTab.INDICES ->
Expand All @@ -109,6 +117,14 @@ fun defaultMarketSortState(
else -> MarketSortState()
}

internal fun scoreMarketSortState() =
MarketSortState(MarketSortColumn.SCORE, MarketSortDirection.DESCENDING)

internal fun isScoreOrderingAvailable(
topTab: MarketTopTab,
subTab: MarketSubTab?,
): Boolean = topTab == MarketTopTab.PERPETUAL && subTab == MarketSubTab.TRENDING

data class MarketDisplaySettings(
val quoteColorReversed: Boolean = false,
val priceChangePeriod: MarketPriceChangePeriod = MarketPriceChangePeriod.SEVEN_DAYS,
Expand Down Expand Up @@ -274,6 +290,7 @@ object MarketPageMapper {
subTab: MarketSubTab,
): List<PerpsMarket> =
when (subTab) {
MarketSubTab.TRENDING -> markets.sortedByDescending(PerpsMarket::tradeVolumeScore1D)
MarketSubTab.TOP_GAINERS -> markets.sortedByDescending { it.changePercentValue() ?: BigDecimal.ZERO }
MarketSubTab.TOP_LOSERS -> markets.sortedBy { it.changePercentValue() ?: BigDecimal.ZERO }
MarketSubTab.INDICES -> markets.filterByCategory(PerpsMarketCategoryKey.INDICES)
Expand Down Expand Up @@ -338,6 +355,7 @@ object MarketPageMapper {
MarketSortColumn.VOLUME -> entry.volume(useMarketCapForSpot)
MarketSortColumn.PRICE -> entry.price()
MarketSortColumn.CHANGE -> entry.changePercent(period)
MarketSortColumn.SCORE -> entry.score()
} ?: BigDecimal.ZERO
}
return if (sortState.direction == MarketSortDirection.ASCENDING) {
Expand Down Expand Up @@ -386,3 +404,9 @@ fun MarketListEntry.changePercent(period: MarketPriceChangePeriod): BigDecimal?
is MarketListEntry.Spot -> market.changePercent(period)
is MarketListEntry.Perpetual -> market.changePercentValue()
}

fun MarketListEntry.score(): BigDecimal? =
when (this) {
is MarketListEntry.Spot -> null
is MarketListEntry.Perpetual -> market.tradeVolumeScore1D.toBigDecimal()
}
Original file line number Diff line number Diff line change
Expand Up @@ -115,15 +115,19 @@ class MarketPageViewModel

fun updateSort(column: MarketSortColumn) {
val state = _uiState.value
val sortState = state.sortState.next(column)
val sortState =
state.sortState.next(
column,
isScoreOrderingAvailable(state.selectedTopTab, state.selectedSubTab),
)
_uiState.value = state.copy(sortState = sortState)
rebuildEntries()
sortState.direction.analyticsValue()?.let { direction ->
AnalyticsTracker.trackMarketsListSort(
sortDirection = direction,
primaryTab = state.selectedTopTab.analyticsValue(),
secondaryTab = state.selectedSubTab.analyticsValue(),
sortField = state.analyticsSortField(column),
sortField = state.analyticsSortField(sortState.column ?: column),
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import one.mixin.android.ui.home.web3.market.MarketSortDirection
import one.mixin.android.ui.home.web3.market.MarketSortState
import one.mixin.android.ui.home.web3.market.PerpsMarketCategoryKey
import one.mixin.android.ui.home.web3.market.changePercentValue
import one.mixin.android.ui.home.web3.market.scoreMarketSortState
import one.mixin.android.ui.home.web3.widget.MarketSort
import java.math.BigDecimal

Expand All @@ -32,7 +33,7 @@ internal enum class PerpsMarketCategory(val categoryKey: PerpsMarketCategoryKey?
internal data class PerpsMarketListUiState(
val query: String = "",
val selectedCategory: PerpsMarketCategory = PerpsMarketCategory.ALL,
val sortState: MarketSortState = defaultPerpsMarketSortState(),
val sortState: MarketSortState = defaultPerpsMarketSortState(PerpsMarketCategory.ALL),
val markets: List<PerpsMarket> = emptyList(),
val favoriteMarketIds: Set<String> = emptySet(),
val favoriteOverrides: Map<String, Boolean> = emptyMap(),
Expand Down Expand Up @@ -76,14 +77,14 @@ internal data class PerpsMarketListUiState(
if (selectedCategory == category) return this
return copy(
selectedCategory = category,
sortState = defaultPerpsMarketSortState(),
sortState = defaultPerpsMarketSortState(category),
scrollToTopRequest = scrollToTopRequest + 1,
).normalizeRecommendationSelection()
}

fun selectSort(column: MarketSortColumn): PerpsMarketListUiState =
copy(
sortState = sortState.next(column),
sortState = sortState.next(column, selectedCategory.isScoreOrderingAvailable),
scrollToTopRequest = scrollToTopRequest + 1,
).normalizeRecommendationSelection()

Expand Down Expand Up @@ -176,14 +177,21 @@ internal data class PerpsMarketListUiState(
}

private fun List<PerpsMarket>.sortedByCurrentState(): List<PerpsMarket> {
val column = sortState.column ?: return this
if (sortState.direction == MarketSortDirection.DEFAULT) return this
val column = sortState.column
if (column == null || sortState.direction == MarketSortDirection.DEFAULT) {
return if (selectedCategory.isScoreOrderingAvailable) {
sortedByDescending(PerpsMarket::tradeVolumeScore1D)
} else {
this
}
}
val comparator =
compareBy<PerpsMarket> { market ->
when (column) {
MarketSortColumn.VOLUME -> market.volume.toBigDecimalOrNull()
MarketSortColumn.PRICE -> market.last.toBigDecimalOrNull()
MarketSortColumn.CHANGE -> market.changePercentValue()
MarketSortColumn.SCORE -> market.tradeVolumeScore1D.toBigDecimal()
} ?: BigDecimal.ZERO
}
return if (sortState.direction == MarketSortDirection.ASCENDING) {
Expand Down Expand Up @@ -222,21 +230,30 @@ internal data class PerpsMarketListUiState(
initialCategory: String?,
initialSort: MarketSort?,
quoteColorReversed: Boolean,
): PerpsMarketListUiState =
PerpsMarketListUiState(
selectedCategory = PerpsMarketCategory.fromInitialCategory(initialCategory),
sortState = initialSort.toPerpsMarketSortState(),
): PerpsMarketListUiState {
val selectedCategory = PerpsMarketCategory.fromInitialCategory(initialCategory)
return PerpsMarketListUiState(
selectedCategory = selectedCategory,
sortState = initialSort.toPerpsMarketSortState(selectedCategory),
quoteColorReversed = quoteColorReversed,
)
}
}
}

internal fun defaultPerpsMarketSortState() =
MarketSortState(MarketSortColumn.VOLUME, MarketSortDirection.DESCENDING)
internal fun defaultPerpsMarketSortState(category: PerpsMarketCategory) =
if (category.isScoreOrderingAvailable) {
scoreMarketSortState()
} else {
MarketSortState(MarketSortColumn.VOLUME, MarketSortDirection.DESCENDING)
}

private val PerpsMarketCategory.isScoreOrderingAvailable: Boolean
get() = this == PerpsMarketCategory.ALL

internal fun MarketSort?.toPerpsMarketSortState(): MarketSortState =
internal fun MarketSort?.toPerpsMarketSortState(category: PerpsMarketCategory): MarketSortState =
when (this) {
null, MarketSort.RANK_DESCENDING -> defaultPerpsMarketSortState()
null, MarketSort.RANK_DESCENDING -> defaultPerpsMarketSortState(category)
MarketSort.RANK_ASCENDING -> MarketSortState(MarketSortColumn.VOLUME, MarketSortDirection.ASCENDING)
MarketSort.PRICE_ASCENDING -> MarketSortState(MarketSortColumn.PRICE, MarketSortDirection.ASCENDING)
MarketSort.PRICE_DESCENDING -> MarketSortState(MarketSortColumn.PRICE, MarketSortDirection.DESCENDING)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,7 @@ object AnalyticsTracker {
const val TWENTY_FOUR_HOURS = "24h"
const val SEVEN_DAYS = "7d"
const val MARKET_CAP = "market_cap"
const val SCORE = "score"
}

object MarketDetailSource {
Expand Down
Loading