diff --git a/app/schemas/googlePlay/one.mixin.android.db.PerpsDatabase/10.json b/app/schemas/googlePlay/one.mixin.android.db.PerpsDatabase/10.json new file mode 100644 index 0000000000..76fb830789 --- /dev/null +++ b/app/schemas/googlePlay/one.mixin.android.db.PerpsDatabase/10.json @@ -0,0 +1,538 @@ +{ + "formatVersion": 1, + "database": { + "version": 10, + "identityHash": "8d3638bfd1234ab92b666272c727970d", + "entities": [ + { + "tableName": "positions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`position_id` TEXT NOT NULL, `market_id` TEXT NOT NULL, `side` TEXT NOT NULL, `quantity` TEXT NOT NULL, `entry_price` TEXT NOT NULL, `margin` TEXT NOT NULL, `leverage` INTEGER NOT NULL, `state` TEXT NOT NULL, `mark_price` TEXT NOT NULL, `unrealized_pnl` TEXT NOT NULL, `roe` TEXT NOT NULL, `settle_asset_id` TEXT NOT NULL, `open_pay_amount` TEXT NOT NULL, `open_pay_asset_id` TEXT NOT NULL, `take_profit_price` TEXT, `stop_loss_price` TEXT, `liquidation_price` TEXT, `bot_id` TEXT NOT NULL, `wallet_id` TEXT NOT NULL, `created_at` TEXT NOT NULL, `updated_at` TEXT NOT NULL, PRIMARY KEY(`position_id`))", + "fields": [ + { + "fieldPath": "positionId", + "columnName": "position_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "marketId", + "columnName": "market_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "side", + "columnName": "side", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "quantity", + "columnName": "quantity", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "entryPrice", + "columnName": "entry_price", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "margin", + "columnName": "margin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "leverage", + "columnName": "leverage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "markPrice", + "columnName": "mark_price", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unrealizedPnl", + "columnName": "unrealized_pnl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roe", + "columnName": "roe", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "settleAssetId", + "columnName": "settle_asset_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "openPayAmount", + "columnName": "open_pay_amount", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "openPayAssetId", + "columnName": "open_pay_asset_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "takeProfitPrice", + "columnName": "take_profit_price", + "affinity": "TEXT" + }, + { + "fieldPath": "stopLossPrice", + "columnName": "stop_loss_price", + "affinity": "TEXT" + }, + { + "fieldPath": "liquidationPrice", + "columnName": "liquidation_price", + "affinity": "TEXT" + }, + { + "fieldPath": "botId", + "columnName": "bot_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "walletId", + "columnName": "wallet_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updated_at", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "position_id" + ] + } + }, + { + "tableName": "perps_orders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`order_id` TEXT NOT NULL, `position_id` TEXT NOT NULL, `market_id` TEXT NOT NULL, `side` TEXT NOT NULL, `order_type` TEXT NOT NULL, `status` TEXT NOT NULL, `leverage` INTEGER NOT NULL, `quantity` TEXT NOT NULL, `pay_amount` TEXT NOT NULL, `fee_amount` TEXT NOT NULL DEFAULT '0', `entry_price` TEXT NOT NULL, `close_price` TEXT NOT NULL, `realized_pnl` TEXT NOT NULL, `roe` TEXT NOT NULL, `net_realized_pnl` TEXT NOT NULL DEFAULT '', `net_roe` TEXT NOT NULL DEFAULT '', `profit_share_amount` TEXT NOT NULL DEFAULT '', `close_reason` TEXT, `trigger_price` TEXT, `created_at` TEXT NOT NULL, `updated_at` TEXT NOT NULL, PRIMARY KEY(`order_id`))", + "fields": [ + { + "fieldPath": "orderId", + "columnName": "order_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "positionId", + "columnName": "position_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "marketId", + "columnName": "market_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "side", + "columnName": "side", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "orderType", + "columnName": "order_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "leverage", + "columnName": "leverage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "quantity", + "columnName": "quantity", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "payAmount", + "columnName": "pay_amount", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "feeAmount", + "columnName": "fee_amount", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'0'" + }, + { + "fieldPath": "entryPrice", + "columnName": "entry_price", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "closePrice", + "columnName": "close_price", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "realizedPnl", + "columnName": "realized_pnl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roe", + "columnName": "roe", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "netRealizedPnl", + "columnName": "net_realized_pnl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "netRoe", + "columnName": "net_roe", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "profitShareAmount", + "columnName": "profit_share_amount", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "closeReason", + "columnName": "close_reason", + "affinity": "TEXT" + }, + { + "fieldPath": "triggerPrice", + "columnName": "trigger_price", + "affinity": "TEXT" + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updated_at", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "order_id" + ] + } + }, + { + "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, `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", + "columnName": "market_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displaySymbol", + "columnName": "display_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tokenSymbol", + "columnName": "token_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "quoteSymbol", + "columnName": "quote_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "markPrice", + "columnName": "mark_price", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "priceScale", + "columnName": "price_scale", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "2" + }, + { + "fieldPath": "leverage", + "columnName": "leverage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "iconUrl", + "columnName": "icon_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fundingRate", + "columnName": "funding_rate", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fundingIntervalHours", + "columnName": "funding_interval_hours", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "nextFundingAt", + "columnName": "next_funding_at", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "openInterest", + "columnName": "open_interest", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'0'" + }, + { + "fieldPath": "minAmount", + "columnName": "min_amount", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "maxAmount", + "columnName": "max_amount", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "last", + "columnName": "last", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "volume", + "columnName": "volume", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tradeVolumeScore1D", + "columnName": "trade_volume_score_1d", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "high", + "columnName": "high", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "low", + "columnName": "low", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "open", + "columnName": "open", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "change", + "columnName": "change", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bidPrice", + "columnName": "bid_price", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "askPrice", + "columnName": "ask_price", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updated_at", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "descriptions", + "columnName": "descriptions", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "market_id" + ] + } + }, + { + "tableName": "favorites", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`market_id` TEXT NOT NULL, `is_favored` INTEGER NOT NULL, `created_at` TEXT NOT NULL, PRIMARY KEY(`market_id`))", + "fields": [ + { + "fieldPath": "marketId", + "columnName": "market_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isFavored", + "columnName": "is_favored", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "market_id" + ] + } + }, + { + "tableName": "market_categories", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`market_id` TEXT NOT NULL, `category` INTEGER NOT NULL, PRIMARY KEY(`market_id`, `category`))", + "fields": [ + { + "fieldPath": "marketId", + "columnName": "market_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "market_id", + "category" + ] + }, + "indices": [ + { + "name": "index_market_categories_category", + "unique": false, + "columnNames": [ + "category" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_market_categories_category` ON `${TABLE_NAME}` (`category`)" + } + ] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8d3638bfd1234ab92b666272c727970d')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/otherChannel/one.mixin.android.db.PerpsDatabase/10.json b/app/schemas/otherChannel/one.mixin.android.db.PerpsDatabase/10.json new file mode 100644 index 0000000000..76fb830789 --- /dev/null +++ b/app/schemas/otherChannel/one.mixin.android.db.PerpsDatabase/10.json @@ -0,0 +1,538 @@ +{ + "formatVersion": 1, + "database": { + "version": 10, + "identityHash": "8d3638bfd1234ab92b666272c727970d", + "entities": [ + { + "tableName": "positions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`position_id` TEXT NOT NULL, `market_id` TEXT NOT NULL, `side` TEXT NOT NULL, `quantity` TEXT NOT NULL, `entry_price` TEXT NOT NULL, `margin` TEXT NOT NULL, `leverage` INTEGER NOT NULL, `state` TEXT NOT NULL, `mark_price` TEXT NOT NULL, `unrealized_pnl` TEXT NOT NULL, `roe` TEXT NOT NULL, `settle_asset_id` TEXT NOT NULL, `open_pay_amount` TEXT NOT NULL, `open_pay_asset_id` TEXT NOT NULL, `take_profit_price` TEXT, `stop_loss_price` TEXT, `liquidation_price` TEXT, `bot_id` TEXT NOT NULL, `wallet_id` TEXT NOT NULL, `created_at` TEXT NOT NULL, `updated_at` TEXT NOT NULL, PRIMARY KEY(`position_id`))", + "fields": [ + { + "fieldPath": "positionId", + "columnName": "position_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "marketId", + "columnName": "market_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "side", + "columnName": "side", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "quantity", + "columnName": "quantity", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "entryPrice", + "columnName": "entry_price", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "margin", + "columnName": "margin", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "leverage", + "columnName": "leverage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "markPrice", + "columnName": "mark_price", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unrealizedPnl", + "columnName": "unrealized_pnl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roe", + "columnName": "roe", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "settleAssetId", + "columnName": "settle_asset_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "openPayAmount", + "columnName": "open_pay_amount", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "openPayAssetId", + "columnName": "open_pay_asset_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "takeProfitPrice", + "columnName": "take_profit_price", + "affinity": "TEXT" + }, + { + "fieldPath": "stopLossPrice", + "columnName": "stop_loss_price", + "affinity": "TEXT" + }, + { + "fieldPath": "liquidationPrice", + "columnName": "liquidation_price", + "affinity": "TEXT" + }, + { + "fieldPath": "botId", + "columnName": "bot_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "walletId", + "columnName": "wallet_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updated_at", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "position_id" + ] + } + }, + { + "tableName": "perps_orders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`order_id` TEXT NOT NULL, `position_id` TEXT NOT NULL, `market_id` TEXT NOT NULL, `side` TEXT NOT NULL, `order_type` TEXT NOT NULL, `status` TEXT NOT NULL, `leverage` INTEGER NOT NULL, `quantity` TEXT NOT NULL, `pay_amount` TEXT NOT NULL, `fee_amount` TEXT NOT NULL DEFAULT '0', `entry_price` TEXT NOT NULL, `close_price` TEXT NOT NULL, `realized_pnl` TEXT NOT NULL, `roe` TEXT NOT NULL, `net_realized_pnl` TEXT NOT NULL DEFAULT '', `net_roe` TEXT NOT NULL DEFAULT '', `profit_share_amount` TEXT NOT NULL DEFAULT '', `close_reason` TEXT, `trigger_price` TEXT, `created_at` TEXT NOT NULL, `updated_at` TEXT NOT NULL, PRIMARY KEY(`order_id`))", + "fields": [ + { + "fieldPath": "orderId", + "columnName": "order_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "positionId", + "columnName": "position_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "marketId", + "columnName": "market_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "side", + "columnName": "side", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "orderType", + "columnName": "order_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "leverage", + "columnName": "leverage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "quantity", + "columnName": "quantity", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "payAmount", + "columnName": "pay_amount", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "feeAmount", + "columnName": "fee_amount", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'0'" + }, + { + "fieldPath": "entryPrice", + "columnName": "entry_price", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "closePrice", + "columnName": "close_price", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "realizedPnl", + "columnName": "realized_pnl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "roe", + "columnName": "roe", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "netRealizedPnl", + "columnName": "net_realized_pnl", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "netRoe", + "columnName": "net_roe", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "profitShareAmount", + "columnName": "profit_share_amount", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "closeReason", + "columnName": "close_reason", + "affinity": "TEXT" + }, + { + "fieldPath": "triggerPrice", + "columnName": "trigger_price", + "affinity": "TEXT" + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updated_at", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "order_id" + ] + } + }, + { + "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, `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", + "columnName": "market_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displaySymbol", + "columnName": "display_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tokenSymbol", + "columnName": "token_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "quoteSymbol", + "columnName": "quote_symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "markPrice", + "columnName": "mark_price", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "priceScale", + "columnName": "price_scale", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "2" + }, + { + "fieldPath": "leverage", + "columnName": "leverage", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "iconUrl", + "columnName": "icon_url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fundingRate", + "columnName": "funding_rate", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "fundingIntervalHours", + "columnName": "funding_interval_hours", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "nextFundingAt", + "columnName": "next_funding_at", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "openInterest", + "columnName": "open_interest", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'0'" + }, + { + "fieldPath": "minAmount", + "columnName": "min_amount", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "maxAmount", + "columnName": "max_amount", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "last", + "columnName": "last", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "volume", + "columnName": "volume", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tradeVolumeScore1D", + "columnName": "trade_volume_score_1d", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "high", + "columnName": "high", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "low", + "columnName": "low", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "open", + "columnName": "open", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "change", + "columnName": "change", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bidPrice", + "columnName": "bid_price", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "askPrice", + "columnName": "ask_price", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "updatedAt", + "columnName": "updated_at", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "descriptions", + "columnName": "descriptions", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "market_id" + ] + } + }, + { + "tableName": "favorites", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`market_id` TEXT NOT NULL, `is_favored` INTEGER NOT NULL, `created_at` TEXT NOT NULL, PRIMARY KEY(`market_id`))", + "fields": [ + { + "fieldPath": "marketId", + "columnName": "market_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isFavored", + "columnName": "is_favored", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "created_at", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "market_id" + ] + } + }, + { + "tableName": "market_categories", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`market_id` TEXT NOT NULL, `category` INTEGER NOT NULL, PRIMARY KEY(`market_id`, `category`))", + "fields": [ + { + "fieldPath": "marketId", + "columnName": "market_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "market_id", + "category" + ] + }, + "indices": [ + { + "name": "index_market_categories_category", + "unique": false, + "columnNames": [ + "category" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_market_categories_category` ON `${TABLE_NAME}` (`category`)" + } + ] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8d3638bfd1234ab92b666272c727970d')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/one/mixin/android/api/response/perps/PerpsExt.kt b/app/src/main/java/one/mixin/android/api/response/perps/PerpsExt.kt index f89b9a01e1..08a26a011f 100644 --- a/app/src/main/java/one/mixin/android/api/response/perps/PerpsExt.kt +++ b/app/src/main/java/one/mixin/android/api/response/perps/PerpsExt.kt @@ -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, diff --git a/app/src/main/java/one/mixin/android/api/response/perps/PerpsOrder.kt b/app/src/main/java/one/mixin/android/api/response/perps/PerpsOrder.kt index e54d36fe6d..70bd70d9f0 100644 --- a/app/src/main/java/one/mixin/android/api/response/perps/PerpsOrder.kt +++ b/app/src/main/java/one/mixin/android/api/response/perps/PerpsOrder.kt @@ -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 = "", + @SerializedName("net_roe") + @ColumnInfo(name = "net_roe", defaultValue = "''") + 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?, diff --git a/app/src/main/java/one/mixin/android/api/response/perps/PerpsOrderItem.kt b/app/src/main/java/one/mixin/android/api/response/perps/PerpsOrderItem.kt index 25ddaacfce..7704f3d4a0 100644 --- a/app/src/main/java/one/mixin/android/api/response/perps/PerpsOrderItem.kt +++ b/app/src/main/java/one/mixin/android/api/response/perps/PerpsOrderItem.kt @@ -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?, diff --git a/app/src/main/java/one/mixin/android/db/PerpsDatabase.kt b/app/src/main/java/one/mixin/android/db/PerpsDatabase.kt index 61282655d5..f56e6c4c83 100644 --- a/app/src/main/java/one/mixin/android/db/PerpsDatabase.kt +++ b/app/src/main/java/one/mixin/android/db/PerpsDatabase.kt @@ -35,7 +35,7 @@ import kotlin.math.min PerpsFavorite::class, PerpsMarketCategoryRelation::class, ], - version = 9, + version = 10, ) @DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class) abstract class PerpsDatabase : RoomDatabase() { @@ -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 ''") + } + } @Suppress("DEPRECATION") fun getDatabase( @@ -143,7 +151,7 @@ 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) { @@ -151,7 +159,7 @@ abstract class PerpsDatabase : RoomDatabase() { 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( diff --git a/app/src/main/java/one/mixin/android/db/perps/PerpsOrderDao.kt b/app/src/main/java/one/mixin/android/db/perps/PerpsOrderDao.kt index 5f55b159ef..0bc2e91fe3 100644 --- a/app/src/main/java/one/mixin/android/db/perps/PerpsOrderDao.kt +++ b/app/src/main/java/one/mixin/android/db/perps/PerpsOrderDao.kt @@ -85,10 +85,10 @@ interface PerpsOrderDao : BaseDao { @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'") 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 @Query("SELECT SUM(CAST(entry_price AS REAL) * ABS(CAST(quantity AS REAL))) FROM perps_orders WHERE order_type = 'close'") diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/ClosedPositionItem.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/ClosedPositionItem.kt index dcc9234f0e..85ed037c0e 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/ClosedPositionItem.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/ClosedPositionItem.kt @@ -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 @@ -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 diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/ClosedActivityItem.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/ClosedActivityItem.kt index 8474737cd9..3a6b6cea4b 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/ClosedActivityItem.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/ClosedActivityItem.kt @@ -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 @@ -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 diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsPositionShareBottomFragment.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsPositionShareBottomFragment.kt index 3655a1e427..1670403ca8 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsPositionShareBottomFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PerpsPositionShareBottomFragment.kt @@ -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 @@ -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, diff --git a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PositionDetailPage.kt b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PositionDetailPage.kt index f916e85a5d..331078c03d 100644 --- a/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PositionDetailPage.kt +++ b/app/src/main/java/one/mixin/android/ui/home/web3/trade/perps/PositionDetailPage.kt @@ -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 @@ -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 @@ -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 { diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 2452a2832d..26dd103d0c 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -2344,7 +2344,7 @@ 开仓、加仓和平仓均会产生手续费,费用按仓位价值计算,而不是按保证金计算。 仓位越大,手续费越高 仓位越小,手续费越低 - 已实现盈亏已扣除相关手续费 + 净盈亏已扣除相关手续费 强平价格 止盈条件 止损条件 @@ -2648,6 +2648,6 @@ 加仓 增加保证金 减少保证金 - 已实现盈亏 - 未实现盈亏 + 净盈亏 + 盈亏 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index fa0869138f..38e106dcee 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -2458,7 +2458,7 @@ 開倉、加倉和平倉均會產生手續費,費用按倉位價值計算,而不是按保證金計算。 倉位越大,手續費越高 倉位越小,手續費越低 - 已實現盈虧已扣除相關手續費 + 淨盈虧已扣除相關手續費 強平價格 價格達到觸發價時自動平倉,及時鎖定利潤 價格跌至觸發價時自動平倉,幫助控制虧損 @@ -2700,6 +2700,6 @@ 加倉 增加保證金 減少保證金 - 已實現盈虧 - 未實現盈虧 + 淨盈虧 + 盈虧 diff --git a/app/src/test/java/one/mixin/android/db/Room3DatabaseCompatibilityTest.kt b/app/src/test/java/one/mixin/android/db/Room3DatabaseCompatibilityTest.kt index a874c7e7fe..5bbee80b9b 100644 --- a/app/src/test/java/one/mixin/android/db/Room3DatabaseCompatibilityTest.kt +++ b/app/src/test/java/one/mixin/android/db/Room3DatabaseCompatibilityTest.kt @@ -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 @@ -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( @@ -92,14 +115,19 @@ class Room3DatabaseCompatibilityTest { selectSql: String, expectedValue: String, schemaName: String = databaseClass.name, + schemaVersion: Int? = null, + migrations: Array = 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 -> @@ -122,13 +150,14 @@ class Room3DatabaseCompatibilityTest { val context = ApplicationProvider.getApplicationContext() 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 { diff --git a/app/src/test/java/one/mixin/android/db/perps/PerpsOrderDaoTest.kt b/app/src/test/java/one/mixin/android/db/perps/PerpsOrderDaoTest.kt index 9eb85b2200..2c84a9e6ef 100644 --- a/app/src/test/java/one/mixin/android/db/perps/PerpsOrderDaoTest.kt +++ b/app/src/test/java/one/mixin/android/db/perps/PerpsOrderDaoTest.kt @@ -4,9 +4,12 @@ import android.content.Context import androidx.paging.PagingSource import androidx.room3.Room import androidx.test.core.app.ApplicationProvider +import com.google.gson.Gson import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import one.mixin.android.api.response.perps.PerpsOrder +import one.mixin.android.api.response.perps.realizedPnlForDisplay +import one.mixin.android.api.response.perps.roeForDisplay import one.mixin.android.db.PerpsDatabase import org.junit.After import org.junit.Assert.assertEquals @@ -15,6 +18,7 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +import java.math.BigDecimal @RunWith(RobolectricTestRunner::class) class PerpsOrderDaoTest { @@ -59,7 +63,6 @@ class PerpsOrderDaoTest { fun syncOffsetIgnoresProcessingOrdersUntilTheyComplete() = runBlocking { val dao = database.perpsOrderDao() assertNull(dao.getLatestUpdatedAt()) - val pending = order("05", PerpsOrder.TYPE_INCREASE_MARGIN).copy(status = PerpsOrder.STATUS_PROCESSING) dao.insertAll(listOf(pending)) assertNull(dao.getLatestUpdatedAt()) @@ -75,6 +78,53 @@ class PerpsOrderDaoTest { assertEquals(pending.orderId, dao.getOrdersByMarket("market").first().orderId) } + @Test + fun netPnlAndProfitShareSurviveStorageAndMissingNetValuesUseLegacyPnl() = runBlocking { + val gson = Gson() + val json = gson.toJsonTree(order("01", PerpsOrder.TYPE_CLOSE).copy(realizedPnl = "10", roe = "0.1")).asJsonObject + listOf("net_realized_pnl", "net_roe", "profit_share_amount").forEach { field -> + assertEquals("", json.get(field).asString) + } + json.addProperty("net_realized_pnl", "8") + json.addProperty("net_roe", "0.08") + json.addProperty("profit_share_amount", "1.25") + val response = gson.fromJson(json, PerpsOrder::class.java) + val dao = database.perpsOrderDao() + dao.insertAll( + listOf( + response, + order("02", PerpsOrder.TYPE_CLOSE).copy(realizedPnl = "10", roe = "0.1"), + response.copy(orderId = "03", netRealizedPnl = "", netRoe = ""), + response.copy(orderId = "04", netRealizedPnl = "0", netRoe = "0"), + ), + ) + + val stored = requireNotNull(dao.getOrder("01")) + assertEquals("8", stored.netRealizedPnl) + assertEquals("0.08", stored.netRoe) + assertEquals("1.25", stored.profitShareAmount) + assertEquals("10", stored.realizedPnl) + assertEquals("0.1", stored.roe) + assertEquals(BigDecimal("8"), stored.realizedPnlForDisplay()) + assertEquals(BigDecimal("0.08"), stored.roeForDisplay()) + val legacy = requireNotNull(dao.getOrder("02")) + assertEquals("", legacy.netRealizedPnl) + assertEquals("", legacy.netRoe) + assertEquals("", legacy.profitShareAmount) + listOf("", "invalid").forEach { missing -> + val fallback = legacy.copy(netRealizedPnl = missing, netRoe = missing) + assertEquals(BigDecimal("10"), fallback.realizedPnlForDisplay()) + assertEquals(BigDecimal("0.1"), fallback.roeForDisplay()) + assertEquals(BigDecimal("10"), stored.copy(netRealizedPnl = missing).realizedPnlForDisplay()) + assertEquals(BigDecimal("0.1"), stored.copy(netRoe = missing).roeForDisplay()) + } + val zero = requireNotNull(dao.getOrder("04")) + assertEquals(BigDecimal.ZERO, zero.realizedPnlForDisplay()) + assertEquals(BigDecimal.ZERO, zero.roeForDisplay()) + assertEquals(28.0, dao.getTotalRealizedPnl()) + assertEquals(28.0, dao.observeTotalRealizedPnl().first(), 0.0) + } + private fun order(id: String, type: String) = PerpsOrder( orderId = id, positionId = "position",