Skip to content
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
538 changes: 538 additions & 0 deletions app/schemas/googlePlay/one.mixin.android.db.PerpsDatabase/10.json

Large diffs are not rendered by default.

538 changes: 538 additions & 0 deletions app/schemas/otherChannel/one.mixin.android.db.PerpsDatabase/10.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
package one.mixin.android.api.response.perps

import java.math.BigDecimal

fun PerpsOrderItem.realizedPnlForDisplay(): BigDecimal =
netRealizedPnl.toBigDecimalOrNull() ?: realizedPnl.toBigDecimalOrNull() ?: BigDecimal.ZERO

fun PerpsOrderItem.roeForDisplay(): BigDecimal? =
netRoe.toBigDecimalOrNull() ?: roe.toBigDecimalOrNull()

fun PerpsPositionItem.toPosition(): PerpsPosition {
return PerpsPosition(
positionId = positionId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ data class PerpsOrder(
@SerializedName("roe")
@ColumnInfo(name = "roe")
val roe: String,
@SerializedName("net_realized_pnl")
@ColumnInfo(name = "net_realized_pnl", defaultValue = "''")
val netRealizedPnl: String = "",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Normalize absent net fields before inserting orders

When an orders response omits any of these new properties—or explicitly returns null for an older order—Gson bypasses Kotlin constructor defaults, so the corresponding non-null property is actually null. upsertSyncedOrders() then inserts it into a TEXT NOT NULL column and the order refresh fails instead of falling back to the legacy PnL. Make the wire fields nullable and normalize them before persistence, or provide a deserializer that supplies the empty-string defaults.

Useful? React with 👍 / 👎.

@SerializedName("net_roe")
@ColumnInfo(name = "net_roe", defaultValue = "''")
Comment on lines +56 to +60
val netRoe: String = "",
@SerializedName("profit_share_amount")
@ColumnInfo(name = "profit_share_amount", defaultValue = "''")
val profitShareAmount: String = "",
@SerializedName("close_reason")
@ColumnInfo(name = "close_reason")
val closeReason: String?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,15 @@ data class PerpsOrderItem(
@SerializedName("roe")
@ColumnInfo(name = "roe")
val roe: String,
@SerializedName("net_realized_pnl")
@ColumnInfo(name = "net_realized_pnl")
val netRealizedPnl: String = "",
@SerializedName("net_roe")
@ColumnInfo(name = "net_roe")
val netRoe: String = "",
@SerializedName("profit_share_amount")
@ColumnInfo(name = "profit_share_amount")
val profitShareAmount: String = "",
@SerializedName("close_reason")
@ColumnInfo(name = "close_reason")
val closeReason: String?,
Expand Down
14 changes: 11 additions & 3 deletions app/src/main/java/one/mixin/android/db/PerpsDatabase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import kotlin.math.min
PerpsFavorite::class,
PerpsMarketCategoryRelation::class,
],
version = 9,
version = 10,
)
@DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class)
abstract class PerpsDatabase : RoomDatabase() {
Expand Down Expand Up @@ -124,6 +124,14 @@ abstract class PerpsDatabase : RoomDatabase() {
connection.execSQL("ALTER TABLE `markets` ADD COLUMN `trade_volume_score_1d` INTEGER NOT NULL DEFAULT 0")
}
}
val MIGRATION_9_10 =
object : Migration(9, 10) {
override suspend fun migrate(connection: SQLiteConnection) {
connection.execSQL("ALTER TABLE `perps_orders` ADD COLUMN `net_realized_pnl` TEXT NOT NULL DEFAULT ''")
connection.execSQL("ALTER TABLE `perps_orders` ADD COLUMN `net_roe` TEXT NOT NULL DEFAULT ''")
connection.execSQL("ALTER TABLE `perps_orders` ADD COLUMN `profit_share_amount` TEXT NOT NULL DEFAULT ''")
Comment on lines +130 to +132

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Backfill net values for migrated orders

On a 9→10 upgrade, every cached order receives empty net fields, but refreshOrders() continues requesting from getLatestUpdatedAt() and therefore only upserts orders returned after the existing high-water mark. Completed historical orders normally never change again, so their new values are never fetched and the history and aggregate permanently fall back to the legacy gross PnL. The migration needs to trigger a one-time full backfill or invalidate the cached orders so all migrated close orders can acquire their net PnL and profit-share data.

Useful? React with 👍 / 👎.

}
}

@Suppress("DEPRECATION")
fun getDatabase(
Expand All @@ -143,15 +151,15 @@ abstract class PerpsDatabase : RoomDatabase() {
context,
PerpsDatabase::class.java,
File(dir, Constants.DataBase.PERPS_DB_NAME).absolutePath,
).setDriver(ReportingAndroidSQLiteDriver("Perps", 9))
).setDriver(ReportingAndroidSQLiteDriver("Perps", 10))
.addCallback(
object : Callback() {
override suspend fun onOpen(connection: SQLiteConnection) {
super.onOpen(connection)
connection.execSQL("PRAGMA synchronous = NORMAL")
}
},
).addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9)
).addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9, MIGRATION_9_10)
.fallbackToDestructiveMigration()
.enableMultiInstanceInvalidation()
.setQueryCoroutineContext(
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/one/mixin/android/db/perps/PerpsOrderDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,10 @@ interface PerpsOrderDao : BaseDao<PerpsOrder> {
@Query("SELECT MAX(updated_at) FROM perps_orders WHERE status IN ('filled', 'rejected', 'closed')")
suspend fun getLatestUpdatedAt(): String?

@Query("SELECT SUM(CAST(realized_pnl AS REAL)) FROM perps_orders WHERE order_type = 'close'")
@Query("SELECT SUM(CAST(COALESCE(NULLIF(net_realized_pnl, ''), realized_pnl) AS REAL)) FROM perps_orders WHERE order_type = 'close'")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Fall back from malformed net PnL in aggregate queries

When net_realized_pnl contains a non-empty malformed value such as "invalid" or whitespace, the display helper falls back to realized_pnl, but this query selects the malformed value and SQLite casts it to 0. Consequently, the total PnL can disagree with every displayed order; both aggregate queries should apply the same numeric-validation fallback as realizedPnlForDisplay().

Useful? React with 👍 / 👎.

suspend fun getTotalRealizedPnl(): Double?

@Query("SELECT COALESCE(SUM(CAST(realized_pnl AS REAL)), 0) FROM perps_orders WHERE order_type = 'close'")
@Query("SELECT COALESCE(SUM(CAST(COALESCE(NULLIF(net_realized_pnl, ''), realized_pnl) AS REAL)), 0) FROM perps_orders WHERE order_type = 'close'")
fun observeTotalRealizedPnl(): Flow<Double>

@Query("SELECT SUM(CAST(entry_price AS REAL) * ABS(CAST(quantity AS REAL))) FROM perps_orders WHERE order_type = 'close'")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import androidx.compose.ui.unit.sp
import one.mixin.android.Constants
import one.mixin.android.R
import one.mixin.android.api.response.perps.PerpsOrderItem
import one.mixin.android.api.response.perps.realizedPnlForDisplay
import one.mixin.android.api.response.perps.roeForDisplay
import one.mixin.android.compose.CoilImage
import one.mixin.android.compose.theme.MixinAppTheme
import one.mixin.android.extension.defaultSharedPreferences
Expand Down Expand Up @@ -62,14 +64,14 @@ fun ClosedPositionItem(
if (quoteColorPref) MixinAppTheme.colors.walletGreen else MixinAppTheme.colors.walletRed
}
val leverageBackgroundColor = sideColor.copy(alpha = 0.1f)
val pnl = order.realizedPnl.toBigDecimalOrNull() ?: BigDecimal.ZERO
val pnl = order.realizedPnlForDisplay()
val isProfit = pnl >= BigDecimal.ZERO
val pnlColor = if (isProfit) {
if (quoteColorPref) MixinAppTheme.colors.walletRed else MixinAppTheme.colors.walletGreen
} else {
if (quoteColorPref) MixinAppTheme.colors.walletGreen else MixinAppTheme.colors.walletRed
}
val pnlPercent = order.roe.toBigDecimalOrNull()?.multiply(BigDecimal(100))
val pnlPercent = order.roeForDisplay()?.multiply(BigDecimal(100))

Row(
modifier = Modifier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import one.mixin.android.Constants
import one.mixin.android.R
import one.mixin.android.api.response.perps.PerpsOrder
import one.mixin.android.api.response.perps.PerpsOrderItem
import one.mixin.android.api.response.perps.realizedPnlForDisplay
import one.mixin.android.api.response.perps.roeForDisplay
import one.mixin.android.compose.CoilImage
import one.mixin.android.compose.theme.MixinAppTheme
import one.mixin.android.extension.defaultSharedPreferences
Expand Down Expand Up @@ -62,14 +64,14 @@ fun ClosedActivityItem(
val leverageDimmed = isFailed || order.status == PerpsOrder.STATUS_PROCESSING
val leverageColor = if (leverageDimmed) MixinAppTheme.colors.textAssist else sideColor
val leverageBackgroundColor = leverageColor.copy(alpha = 0.1f)
val pnl = order.realizedPnl.toBigDecimalOrNull() ?: BigDecimal.ZERO
val pnl = order.realizedPnlForDisplay()
val isProfit = pnl >= BigDecimal.ZERO
val pnlColor = if (isProfit) {
if (quoteColorPref) MixinAppTheme.colors.walletRed else MixinAppTheme.colors.walletGreen
} else {
if (quoteColorPref) MixinAppTheme.colors.walletGreen else MixinAppTheme.colors.walletRed
}
val pnlPercent = order.roe.toBigDecimalOrNull()?.multiply(BigDecimal(100))
val pnlPercent = order.roeForDisplay()?.multiply(BigDecimal(100))
val titleRes = when {
isFailed && isLong -> R.string.Closed_Long_Failed
isFailed -> R.string.Closed_Short_Failed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import one.mixin.android.api.referral.buildReferralShareUrl
import one.mixin.android.api.response.perps.PerpsOrder
import one.mixin.android.api.response.perps.PerpsOrderItem
import one.mixin.android.api.response.perps.PerpsPositionItem
import one.mixin.android.api.response.perps.realizedPnlForDisplay
import one.mixin.android.api.response.perps.roeForDisplay
import one.mixin.android.databinding.FragmentPerpsPositionShareBottomBinding
import one.mixin.android.databinding.ItemPerpsPositionSharePosterBinding
import one.mixin.android.extension.defaultSharedPreferences
Expand Down Expand Up @@ -227,9 +229,9 @@ class PerpsPositionShareBottomFragment : MixinComposeBottomSheetDialogFragment()

val closed = closeOrder ?: return false
if (closed.orderType != PerpsOrder.TYPE_CLOSE) return false
val pnlAmount = closed.realizedPnl.toBigDecimalSafely() ?: BigDecimal.ZERO
val pnlAmount = closed.realizedPnlForDisplay()
val effectiveLeverage = if (closed.leverage > 0) closed.leverage else 1
val pnlPercent = (closed.roe.toBigDecimalSafely() ?: BigDecimal.ZERO).multiply(BigDecimal(100))
val pnlPercent = (closed.roeForDisplay() ?: BigDecimal.ZERO).multiply(BigDecimal(100))
bindCardData(
marketId = closed.marketId,
iconUrl = closed.iconUrl,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ import one.mixin.android.api.response.perps.PerpsOrder
import one.mixin.android.api.response.perps.PerpsOrderItem
import one.mixin.android.api.response.perps.PerpsPosition
import one.mixin.android.api.response.perps.PerpsPositionItem
import one.mixin.android.api.response.perps.realizedPnlForDisplay
import one.mixin.android.api.response.perps.roeForDisplay
import one.mixin.android.compose.CoilImage
import one.mixin.android.compose.theme.MixinAppTheme
import one.mixin.android.ui.home.web3.components.PageScaffold
Expand Down Expand Up @@ -403,11 +405,7 @@ fun PositionDetailPage(
}
}

val pnl = try {
BigDecimal(closeOrder.realizedPnl)
} catch (e: Exception) {
BigDecimal.ZERO
}
val pnl = closeOrder.realizedPnlForDisplay()

val isProfit = pnl >= BigDecimal.ZERO
val risingColor = if (quoteColorReversed) MixinAppTheme.colors.walletRed else MixinAppTheme.colors.walletGreen
Expand Down Expand Up @@ -438,7 +436,7 @@ fun PositionDetailPage(
val quantity = closeOrder.quantity.toBigDecimalOrNull() ?: BigDecimal.ZERO
val absQuantity = quantity.abs()
val effectiveLeverage = leverage ?: closeOrder.leverage
val roe = (closeOrder.roe.toBigDecimalOrNull() ?: BigDecimal.ZERO).multiply(BigDecimal(100))
val roe = (closeOrder.roeForDisplay() ?: BigDecimal.ZERO).multiply(BigDecimal(100))
val fee = closeOrder.feeAmount.toBigDecimalOrNull()?.abs() ?: BigDecimal.ZERO

fun formatFiat(value: BigDecimal): String {
Expand Down
6 changes: 3 additions & 3 deletions app/src/main/res/values-zh-rCN/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2344,7 +2344,7 @@
<string name="perps_trading_fee_overview">开仓、加仓和平仓均会产生手续费,费用按仓位价值计算,而不是按保证金计算。</string>
<string name="perps_trading_fee_pnl_1">仓位越大,手续费越高</string>
<string name="perps_trading_fee_pnl_2">仓位越小,手续费越低</string>
<string name="perps_trading_fee_pnl_3">已实现盈亏已扣除相关手续费</string>
<string name="perps_trading_fee_pnl_3">净盈亏已扣除相关手续费</string>
<string name="perps_liquidation_price_short">强平价格</string>
<string name="Take_Profit_When">止盈条件</string>
<string name="Stop_Loss_When">止损条件</string>
Expand Down Expand Up @@ -2648,6 +2648,6 @@
<string name="perps_added_position">加仓</string>
<string name="perps_added_margin">增加保证金</string>
<string name="perps_reduced_margin">减少保证金</string>
<string name="perps_realized_pnl">已实现盈亏</string>
<string name="perps_unrealized_pnl">未实现盈亏</string>
<string name="perps_realized_pnl">净盈亏</string>
<string name="perps_unrealized_pnl">盈亏</string>
</resources>
6 changes: 3 additions & 3 deletions app/src/main/res/values-zh-rTW/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2458,7 +2458,7 @@
<string name="perps_trading_fee_overview">開倉、加倉和平倉均會產生手續費,費用按倉位價值計算,而不是按保證金計算。</string>
<string name="perps_trading_fee_pnl_1">倉位越大,手續費越高</string>
<string name="perps_trading_fee_pnl_2">倉位越小,手續費越低</string>
<string name="perps_trading_fee_pnl_3">已實現盈虧已扣除相關手續費</string>
<string name="perps_trading_fee_pnl_3">淨盈虧已扣除相關手續費</string>
<string name="perps_liquidation_price_short">強平價格</string>
<string name="lock_in_profits_description">價格達到觸發價時自動平倉,及時鎖定利潤</string>
<string name="prevent_further_loss_description">價格跌至觸發價時自動平倉,幫助控制虧損</string>
Expand Down Expand Up @@ -2700,6 +2700,6 @@
<string name="perps_added_position">加倉</string>
<string name="perps_added_margin">增加保證金</string>
<string name="perps_reduced_margin">減少保證金</string>
<string name="perps_realized_pnl">已實現盈虧</string>
<string name="perps_unrealized_pnl">未實現盈虧</string>
<string name="perps_realized_pnl">淨盈虧</string>
<string name="perps_unrealized_pnl">盈虧</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.content.Context
import android.database.sqlite.SQLiteDatabase
import androidx.room3.Room
import androidx.room3.RoomDatabase
import androidx.room3.migration.Migration
import androidx.test.core.app.ApplicationProvider
import com.google.gson.JsonParser
import java.io.File
Expand Down Expand Up @@ -55,6 +56,28 @@ class Room3DatabaseCompatibilityTest {
)
}

@Test
fun perpsNetPnlMigrationPreservesOrdersWithoutInventingNetValues() {
verifyLegacyDatabase(
PerpsDatabase::class.java,
"""
INSERT INTO perps_orders (
order_id, position_id, market_id, side, order_type, status, leverage,
quantity, pay_amount, fee_amount, entry_price, close_price, realized_pnl,
roe, created_at, updated_at
) VALUES (
'order', 'position', 'market', 'long', 'close', 'filled', 10,
'1', '10', '2', '100', '110', '10', '0.1',
'2026-09-28T00:00:00Z', '2026-09-28T00:00:00Z'
)
""".trimIndent(),
"SELECT realized_pnl || ':' || roe || ':' || COALESCE(net_realized_pnl, 'missing') || ':' || COALESCE(net_roe, 'missing') || ':' || COALESCE(profit_share_amount, 'missing') FROM perps_orders",
"10:0.1:::",
schemaVersion = 9,
migrations = arrayOf(PerpsDatabase.MIGRATION_9_10),
)
}

@Test
fun signalDatabasePreservesLegacyKeyBytes() {
verifyLegacyDatabase(
Expand Down Expand Up @@ -92,14 +115,19 @@ class Room3DatabaseCompatibilityTest {
selectSql: String,
expectedValue: String,
schemaName: String = databaseClass.name,
schemaVersion: Int? = null,
migrations: Array<Migration> = emptyArray(),
) {
val schemaDirectory = listOf(File("schemas/googlePlay/$schemaName"), File("app/schemas/googlePlay/$schemaName"))
.firstOrNull(File::isDirectory)
requireNotNull(schemaDirectory) { "Missing exported schema for $schemaName in ${File(".").absolutePath}" }
val schemaFile = requireNotNull(schemaDirectory.listFiles()?.filter { it.extension == "json" }?.maxByOrNull { it.nameWithoutExtension.toInt() })
val latestSchemaFile = requireNotNull(schemaDirectory.listFiles()?.filter { it.extension == "json" }?.maxByOrNull { it.nameWithoutExtension.toInt() })
val latestSchema = JsonParser.parseString(latestSchemaFile.readText()).asJsonObject.getAsJsonObject("database")
val schemaFile = schemaVersion?.let { File(schemaDirectory, "$it.json") } ?: latestSchemaFile
val schema = JsonParser.parseString(schemaFile.readText()).asJsonObject.getAsJsonObject("database")
val version = schema.get("version").asInt
val identityHash = schema.get("identityHash").asString
val targetVersion = latestSchema.get("version").asInt
val identityHash = latestSchema.get("identityHash").asString
val file = File(temporaryFolder.root, "${databaseClass.simpleName}.db")
SQLiteDatabase.openOrCreateDatabase(file, null).use { legacy ->
schema.getAsJsonArray("entities").forEach { entityElement ->
Expand All @@ -122,13 +150,14 @@ class Room3DatabaseCompatibilityTest {
val context = ApplicationProvider.getApplicationContext<Context>()
repeat(2) {
val database = Room.databaseBuilder(context, databaseClass, file.absolutePath)
.setDriver(ReportingAndroidSQLiteDriver(databaseClass.simpleName, version))
.setDriver(ReportingAndroidSQLiteDriver(databaseClass.simpleName, targetVersion))
.addMigrations(*migrations)
.allowMainThreadQueries()
.build()
try {
assertScalar(database, selectSql, expectedValue)
assertScalar(database, "SELECT identity_hash FROM room_master_table WHERE id = 42", identityHash)
assertScalar(database, "PRAGMA user_version", version.toString())
assertScalar(database, "PRAGMA user_version", targetVersion.toString())
assertScalar(database, "PRAGMA integrity_check", "ok")
RoomDatabaseCompat.query(database, "PRAGMA foreign_key_check").use { assertFalse(it.moveToFirst()) }
} finally {
Expand Down
Loading
Loading