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
Original file line number Diff line number Diff line change
Expand Up @@ -2185,6 +2185,16 @@ fun mockPermit2ApprovalData(
permit2Nonce = permit2Nonce,
)

fun mockPerpetualClosingPosition(
pnl: Double = 0.0,
entryPrice: Double = 0.0,
initialMargin: Double = 0.0,
) = uniffi.gemstone.PerpetualClosingPosition(
pnl = pnl,
entryPrice = entryPrice,
initialMargin = initialMargin,
)

fun mockPerpetualConfirmData(
direction: uniffi.gemstone.PerpetualDirection = uniffi.gemstone.PerpetualDirection.SHORT,
marginType: uniffi.gemstone.PerpetualMarginType = uniffi.gemstone.PerpetualMarginType.CROSS,
Expand All @@ -2195,8 +2205,7 @@ fun mockPerpetualConfirmData(
size: String = "",
slippage: Double = 0.0,
leverage: UByte = 0u,
pnl: Double? = null,
entryPrice: Double? = null,
closing: uniffi.gemstone.PerpetualClosingPosition? = null,
marketPrice: Double = 0.0,
marginAmount: Double = 0.0,
takeProfit: String? = null,
Expand All @@ -2211,8 +2220,7 @@ fun mockPerpetualConfirmData(
size = size,
slippage = slippage,
leverage = leverage,
pnl = pnl,
entryPrice = entryPrice,
closing = closing,
marketPrice = marketPrice,
marginAmount = marginAmount,
takeProfit = takeProfit,
Expand Down
2 changes: 2 additions & 0 deletions core/bin/generate/remote_types.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ remote:
- PerpetualPrice
- PerpetualProvider
- PerpetualTriggerOrder
- PerpetualClosingPosition
- PerpetualConfirmData
- CancelOrderData
- TPSLOrderData
Expand Down Expand Up @@ -358,6 +359,7 @@ mocks:
- PaymentPrice
- PaymentQuote
- Perpetual
- PerpetualClosingPosition
- PerpetualConfirmData
- PerpetualData
- PerpetualMetadata
Expand Down
4 changes: 2 additions & 2 deletions core/crates/primitives/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,8 +283,8 @@ pub use self::asset_address::AssetAddress;
pub mod graphql;
pub mod perpetual;
pub use self::perpetual::{
AccountDataType, CancelOrderData, Perpetual, PerpetualAccountMode, PerpetualBalance, PerpetualBasic, PerpetualConfirmData, PerpetualData, PerpetualDirection, PerpetualMarketData, PerpetualMetadata, PerpetualModifyConfirmData,
PerpetualModifyPositionType, PerpetualPositionData, PerpetualPositionsSummary, PerpetualPrice, PerpetualReduceData, PerpetualSearchData, PerpetualType, TPSLOrderData,
AccountDataType, CancelOrderData, Perpetual, PerpetualAccountMode, PerpetualBalance, PerpetualBasic, PerpetualClosingPosition, PerpetualConfirmData, PerpetualData, PerpetualDirection, PerpetualMarketData, PerpetualMetadata,
PerpetualModifyConfirmData, PerpetualModifyPositionType, PerpetualPositionData, PerpetualPositionsSummary, PerpetualPrice, PerpetualReduceData, PerpetualSearchData, PerpetualType, TPSLOrderData,
};
pub mod search;
pub use self::search::{AssetList, SearchResponse};
Expand Down
10 changes: 8 additions & 2 deletions core/crates/primitives/src/perpetual.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,14 +138,20 @@ pub struct PerpetualConfirmData {
pub size: String,
pub slippage: f64,
pub leverage: u8,
pub pnl: Option<f64>,
pub entry_price: Option<f64>,
pub closing: Option<PerpetualClosingPosition>,
pub market_price: f64,
pub margin_amount: f64,
pub take_profit: Option<String>,
pub stop_loss: Option<String>,
}

#[derive(Debug, Clone, PartialEq)]
pub struct PerpetualClosingPosition {
pub pnl: f64,
pub entry_price: f64,
pub initial_margin: f64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum AccountDataType {
Expand Down
3 changes: 1 addition & 2 deletions core/crates/primitives/src/testkit/perpetual_mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ impl PerpetualConfirmData {
size: "2.5".to_string(),
slippage: 0.01,
leverage: 5,
pnl: None,
entry_price: None,
closing: None,
market_price: 123.45,
margin_amount: 50.0,
take_profit,
Expand Down
29 changes: 18 additions & 11 deletions core/gemstone/src/models/remote_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,16 @@ use primitives::{
CoreListItemBadge, CoreListItemIcon, Currency, Delegation, DelegationBase, DelegationState, DelegationValidator, DeviceLocale, EarnType, FeePriority, FeeUnitType, FiatProvider, FiatProviderName, FiatQuote, FiatQuoteType, FiatQuoteUrl,
FiatRate, FiatTransaction, FiatTransactionAssetData, FiatTransactionData, FiatTransactionStatus, GasPriceType, InAppNotification, Latency, LatencyType, LinkType, NFTAsset, NFTAssetData, NFTAttribute, NFTAttributeType, NFTCollection,
NFTData, NFTImages, NFTResource, NFTType, NameProvider, NameRecord, Node, NodeState, Payment, PaymentAmount, PaymentInvoice, PaymentLink, PaymentMerchant, PaymentPrice, PaymentQuote, PaymentRequest, PaymentStatus, PaymentType,
PaymentVerification, Permit2ApprovalData, Perpetual, PerpetualAccountMode, PerpetualBalance, PerpetualBasic, PerpetualConfirmData, PerpetualData, PerpetualDirection, PerpetualMarginType, PerpetualMarketData, PerpetualMetadata,
PerpetualModifyConfirmData, PerpetualModifyPositionType, PerpetualOrderType, PerpetualPosition, PerpetualPositionData, PerpetualPrice, PerpetualProvider, PerpetualReduceData, PerpetualSearchData, PerpetualTriggerOrder, PerpetualType,
Platform, PlatformStore, PortfolioChartData, PortfolioChartType, PortfolioData, PortfolioMarginUsage, PortfolioStatistic, PortfolioType, Price, PriceAlert, PriceAlertData, PriceAlertDirection, PriceAlertNotificationType, PriceProvider,
RecentActivityType, RecentAsset, RedelegateData, RedemptionResult, RedemptionStatus, ReferralAllowance, ReferralQuota, Release, ReportReason, Resource, RewardRedemption, RewardRedemptionOption, RewardRedemptionType, RewardStatus,
Rewards, SimulationBalanceChange, SimulationHeader, SimulationPayloadField, SimulationPayloadFieldDisplay, SimulationPayloadFieldKind, SimulationPayloadFieldType, SimulationResult, SimulationSeverity, SimulationWarning,
SimulationWarningApproval, SimulationWarningType, StakeProviderType, StakeType, SupportAgent, SupportMessage, SupportMessageImage, SupportMessageSender, SupportMessageStatus, SupportTyping, SupportTypingStatus, SwapData,
SwapPriceImpact, SwapPriceImpactType, SwapProvider, SwapProviderData, SwapQuote, SwapQuoteData, SwapQuoteDataType, TPSLOrderData, TotalFiatValue, TpslType, Transaction, TransactionDirection, TransactionExtended, TransactionInputType,
TransactionListItem, TransactionState, TransactionType, TransactionUtxoInput, TransactionsFilter, TransferDataExtra, TransferDataOutputAction, TransferDataOutputType, VerificationStatus, Wallet, WalletAddressItem, WalletConnection,
WalletConnectionSession, WalletConnectionSessionProposal, WalletConnectionState, WalletConnectionVerificationStatus, WalletListItem, WalletSource, WalletType, YieldProvider,
PaymentVerification, Permit2ApprovalData, Perpetual, PerpetualAccountMode, PerpetualBalance, PerpetualBasic, PerpetualClosingPosition, PerpetualConfirmData, PerpetualData, PerpetualDirection, PerpetualMarginType, PerpetualMarketData,
PerpetualMetadata, PerpetualModifyConfirmData, PerpetualModifyPositionType, PerpetualOrderType, PerpetualPosition, PerpetualPositionData, PerpetualPrice, PerpetualProvider, PerpetualReduceData, PerpetualSearchData,
PerpetualTriggerOrder, PerpetualType, Platform, PlatformStore, PortfolioChartData, PortfolioChartType, PortfolioData, PortfolioMarginUsage, PortfolioStatistic, PortfolioType, Price, PriceAlert, PriceAlertData, PriceAlertDirection,
PriceAlertNotificationType, PriceProvider, RecentActivityType, RecentAsset, RedelegateData, RedemptionResult, RedemptionStatus, ReferralAllowance, ReferralQuota, Release, ReportReason, Resource, RewardRedemption,
RewardRedemptionOption, RewardRedemptionType, RewardStatus, Rewards, SimulationBalanceChange, SimulationHeader, SimulationPayloadField, SimulationPayloadFieldDisplay, SimulationPayloadFieldKind, SimulationPayloadFieldType,
SimulationResult, SimulationSeverity, SimulationWarning, SimulationWarningApproval, SimulationWarningType, StakeProviderType, StakeType, SupportAgent, SupportMessage, SupportMessageImage, SupportMessageSender, SupportMessageStatus,
SupportTyping, SupportTypingStatus, SwapData, SwapPriceImpact, SwapPriceImpactType, SwapProvider, SwapProviderData, SwapQuote, SwapQuoteData, SwapQuoteDataType, TPSLOrderData, TotalFiatValue, TpslType, Transaction,
TransactionDirection, TransactionExtended, TransactionInputType, TransactionListItem, TransactionState, TransactionType, TransactionUtxoInput, TransactionsFilter, TransferDataExtra, TransferDataOutputAction, TransferDataOutputType,
VerificationStatus, Wallet, WalletAddressItem, WalletConnection, WalletConnectionSession, WalletConnectionSessionProposal, WalletConnectionState, WalletConnectionVerificationStatus, WalletListItem, WalletSource, WalletType,
YieldProvider,
};
use std::str::FromStr;

Expand Down Expand Up @@ -1379,6 +1380,13 @@ pub struct PerpetualBasic {
pub provider: PerpetualProvider,
}

#[uniffi::remote(Record)]
pub struct PerpetualClosingPosition {
pub pnl: f64,
pub entry_price: f64,
pub initial_margin: f64,
}

#[uniffi::remote(Record)]
pub struct PerpetualConfirmData {
pub direction: PerpetualDirection,
Expand All @@ -1390,8 +1398,7 @@ pub struct PerpetualConfirmData {
pub size: String,
pub slippage: f64,
pub leverage: u8,
pub pnl: Option<f64>,
pub entry_price: Option<f64>,
pub closing: Option<PerpetualClosingPosition>,
pub market_price: f64,
pub margin_amount: f64,
pub take_profit: Option<String>,
Expand Down
62 changes: 45 additions & 17 deletions core/gemstone/src/services/perpetual/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ use crate::services::error::GemServiceError;
use crate::services::localization::{GemLocalizedText, GemPositionChange, GemTriggerOrder};
use crate::services::transfer::GemTransferData;
use num_bigint::BigUint;
use primitives::{PerpetualConfirmData, PerpetualModifyConfirmData, PerpetualModifyPositionType, PerpetualReduceData, PerpetualType};
use primitives::{PerpetualClosingPosition, PerpetualConfirmData, PerpetualModifyConfirmData, PerpetualModifyPositionType, PerpetualReduceData, PerpetualType};
use std::collections::{HashMap, HashSet};

use crate::models::asset::wallet_default_assets;
Expand Down Expand Up @@ -85,7 +85,7 @@ pub fn confirm_details(asset: &Asset, perpetual_type: &PerpetualType) -> Option<
PerpetualType::Reduce { data } => (data.position_direction.clone(), &data.data, SummarisedBy::Change(GemPositionChange::Reduce)),
PerpetualType::Modify { .. } => return None,
};
let pnl = data.pnl.map(|pnl| pnl_text(pnl, data.margin_amount));
let pnl = data.closing.as_ref().map(|closing| pnl_text(closing.pnl, closing.initial_margin));
let summary = match summarised_by {
SummarisedBy::Position => GemPerpetualConfirmDetailsSummary {
text: Some(position_text(&direction, data.leverage)),
Expand Down Expand Up @@ -129,8 +129,8 @@ fn details_sections(asset: &Asset, direction: &PerpetualDirection, data: &Perpet
}

let mut price_rows = vec![amount(GemListRowTitle::MarketPrice, display_price(asset, data.market_price))];
if let Some(entry_price) = data.entry_price {
price_rows.push(amount(GemListRowTitle::EntryPrice, display_price(asset, entry_price)));
if let Some(closing) = &data.closing {
price_rows.push(amount(GemListRowTitle::EntryPrice, display_price(asset, closing.entry_price)));
}
price_rows.push(amount(GemListRowTitle::Slippage, GemFormattedNumber::percentage(data.slippage, GemPercentageStyle::Unsigned)));

Expand Down Expand Up @@ -211,18 +211,22 @@ fn direction_tone(direction: &PerpetualDirection) -> GemValueTone {
}
}

fn pnl_text(pnl: f64, margin_amount: f64) -> (GemLocalizedText, GemValueTone) {
fn pnl_text(pnl: f64, initial_margin: f64) -> (GemLocalizedText, GemValueTone) {
let amount = GemFormattedNumber::signed_usd(pnl);
let tone = amount.tone;
(
GemLocalizedText::Pnl {
amount,
percent: GemFormattedNumber::percentage(PriceChangeCalculator::pnl_percentage(pnl, margin_amount), GemPercentageStyle::Signed),
percent: GemFormattedNumber::percentage(PriceChangeCalculator::pnl_percentage(pnl, initial_margin), GemPercentageStyle::Signed),
},
tone,
)
}

fn initial_margin(entry_price: f64, size: f64, leverage: u8) -> f64 {
entry_price * size / f64::from(leverage)
}

pub fn autoclose_row(asset: &Asset, data: &PerpetualModifyConfirmData) -> Option<GemListRow> {
let orders = data.modify_types.iter().find_map(|modify| match modify {
PerpetualModifyPositionType::Tpsl { order } => Some(order),
Expand Down Expand Up @@ -489,8 +493,7 @@ pub fn order(provider: PerpetualProvider, input: GemPerpetualOrderInput) -> Perp
size: formatter.format_size(size, input.asset.decimals),
slippage,
leverage: input.leverage,
pnl: None,
entry_price: None,
closing: None,
market_price: input.price,
margin_amount,
take_profit: input.take_profit,
Expand Down Expand Up @@ -632,8 +635,11 @@ pub fn close_order(provider: PerpetualProvider, input: GemPerpetualCloseInput) -
size: formatter.format_size(size, input.asset.decimals),
slippage,
leverage: input.leverage,
pnl: Some(input.pnl),
entry_price: Some(input.entry_price),
closing: Some(PerpetualClosingPosition {
pnl: input.pnl,
entry_price: input.entry_price,
initial_margin: initial_margin(input.entry_price, size, input.leverage),
}),
market_price: input.market_price,
margin_amount: input.margin_amount,
take_profit: None,
Expand Down Expand Up @@ -743,7 +749,7 @@ pub fn position_row(perpetual: &Perpetual, asset: &Asset, position: &PerpetualPo
}

fn position_line(perpetual: &Perpetual, asset: &Asset, position: &PerpetualPosition) -> PerpetualPositionLine {
let (pnl, pnl_tone) = pnl_text(position.pnl, position.margin_amount);
let (pnl, pnl_tone) = pnl_text(position.pnl, initial_margin(position.entry_price, position.size, position.leverage));
PerpetualPositionLine {
icon: crate::services::assets::icon::asset_icon(&asset.id),
id: position.id.clone(),
Expand Down Expand Up @@ -786,7 +792,10 @@ fn position_details(asset: &Asset, position: &PerpetualPosition) -> Vec<GemPerpe
let label = |title: GemListRowTitle, text: GemLocalizedText, tone: GemValueTone, info: Option<GemInfoTopic>| GemListRow::Label { title, text, tone, info, progress: false };
let pnl = GemLocalizedText::Pnl {
amount: GemFormattedNumber::signed_usd(position.pnl),
percent: GemFormattedNumber::percentage(PriceChangeCalculator::pnl_percentage(position.pnl, position.margin_amount), GemPercentageStyle::Signed),
percent: GemFormattedNumber::percentage(
PriceChangeCalculator::pnl_percentage(position.pnl, initial_margin(position.entry_price, position.size, position.leverage)),
GemPercentageStyle::Signed,
),
};
let margin = GemLocalizedText::Margin {
amount: GemFormattedNumber::usd(position.margin_amount),
Expand Down Expand Up @@ -1156,6 +1165,7 @@ mod tests {
fn test_position_details_carry_finished_values() {
let position = PerpetualPosition {
pnl: 5.0,
margin_amount: 25.0,
funding: Some(-1.5),
take_profit: Some(primitives::PerpetualTriggerOrder {
price: 120.0,
Expand Down Expand Up @@ -1536,7 +1546,14 @@ mod tests {
assert_eq!(row.margin.unit, crate::formatted_number::GemNumberUnit::Currency { code: "USD".to_string() });
assert_eq!(row.pnl_tone, GemValueTone::Negative, "a losing position reads as a loss on both apps");
assert_eq!(row.direction_tone, GemValueTone::Negative, "a short reads red on both apps");
assert!(matches!(&row.pnl, GemLocalizedText::Pnl { amount, .. } if amount.value == -0.25));
assert_eq!(
row.pnl,
GemLocalizedText::Pnl {
amount: GemFormattedNumber::signed_usd(-0.25),
percent: GemFormattedNumber::percentage(-1.25, GemPercentageStyle::Signed),
},
"the percent is the return on the margin the position opened with, as Hyperliquid and Auto Close read it"
);
assert_eq!(
row.position,
GemLocalizedText::Position {
Expand Down Expand Up @@ -1885,8 +1902,12 @@ mod tests {
#[test]
fn test_confirm_details_sections_carry_finished_rows_and_the_trigger_prices_the_provider_sent() {
let data = PerpetualConfirmData {
pnl: Some(5.0),
entry_price: Some(100.0),
closing: Some(PerpetualClosingPosition {
pnl: 5.0,
entry_price: 100.0,
initial_margin: 50.0,
}),
margin_amount: 62.5,
..PerpetualConfirmData::mock(PerpetualDirection::Long, 0, Some("12.345".to_string()), None)
};
let sections = confirm_details(&Asset::mock_perpetual(), &PerpetualType::Close { data }).unwrap().sections;
Expand Down Expand Up @@ -2035,8 +2056,15 @@ mod tests {
},
);

assert_eq!(data.pnl, Some(12.5));
assert_eq!(data.entry_price, Some(90.0));
assert_eq!(
data.closing,
Some(PerpetualClosingPosition {
pnl: 12.5,
entry_price: 90.0,
initial_margin: 45.0,
}),
"the margin the position opened with, not the margin in use"
);
assert_eq!(data.fiat_value, 196.0);
}

Expand Down
1 change: 1 addition & 0 deletions docs/product/perpetuals.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ flowchart LR
| A position's market is not stored yet | the balance still updates; the position appears once the markets refresh | a balance never waits on a position |
| A market's price moves while the list is open | its 24h change moves with it | the change next to the price must agree with it and with Hyperliquid |
| A perpetual price shows (the chart, a market row, a position's Entry, Liquidation and Auto Close prices, Auto Close, confirmation, activity) | in Hyperliquid's places for that market: five significant figures and at most six minus the market's size decimals, so XRP reads `$1.4251` and BTC `$123,456`; the chart's axis and tooltip keep the places of the latest close | a market near a dollar moves in the fourth place, which two places hid; the price must read the same as on Hyperliquid |
| A position's PnL percent shows (the Positions list, the position, the close confirmation) | the return on the margin the position opened with, its entry value over its leverage, so a `5x` long whose price rose `10%` reads `+50%` | it must read as Hyperliquid's ROE and as the percent the user set in Auto Close; the margin in use grows and shrinks with the PnL, so against it a gain reads smaller and a loss larger |
| The user long-presses a market | it is pinned | |
| The user searches | positions and markets are filtered | |
| The market has fewer than 14 candles | the chart draws them at the width of 14, newest on the right | wider bodies stop reading as a trend |
Expand Down
20 changes: 16 additions & 4 deletions ios/Packages/GemstonePrimitives/TestKit/GeneratedMocks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1630,6 +1630,20 @@ public extension Gemstone.Permit2ApprovalData {
}
}

public extension Gemstone.PerpetualClosingPosition {
static func mock(
pnl: Double = 0,
entryPrice: Double = 0,
initialMargin: Double = 0,
) -> Gemstone.PerpetualClosingPosition {
Gemstone.PerpetualClosingPosition(
pnl: pnl,
entryPrice: entryPrice,
initialMargin: initialMargin,
)
}
}

public extension Gemstone.PerpetualConfirmData {
static func mock(
direction: Gemstone.PerpetualDirection = .short,
Expand All @@ -1641,8 +1655,7 @@ public extension Gemstone.PerpetualConfirmData {
size: String = "",
slippage: Double = 0,
leverage: UInt8 = 0,
pnl: Double? = nil,
entryPrice: Double? = nil,
closing: Gemstone.PerpetualClosingPosition? = nil,
marketPrice: Double = 0,
marginAmount: Double = 0,
takeProfit: String? = nil,
Expand All @@ -1658,8 +1671,7 @@ public extension Gemstone.PerpetualConfirmData {
size: size,
slippage: slippage,
leverage: leverage,
pnl: pnl,
entryPrice: entryPrice,
closing: closing,
marketPrice: marketPrice,
marginAmount: marginAmount,
takeProfit: takeProfit,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ import Testing
struct PerpetualDetailsViewModelTests {
@Test
func summaryReadsTheCoreText() {
#expect(PerpetualDetailsViewModel.mock(.close(data: .mock(pnl: 500, marginAmount: 1000))).listItemModel.subtitle == "+$500.00 (+50.00%)")
#expect(PerpetualDetailsViewModel.mock(.close(data: .mock(closing: .mock(pnl: 500, initialMargin: 1000)))).listItemModel.subtitle == "+$500.00 (+50.00%)")
}
}
Loading