diff --git a/mobile/lib/common/domain/model.dart b/mobile/lib/common/domain/model.dart index 5d3bddc8e..88050b349 100644 --- a/mobile/lib/common/domain/model.dart +++ b/mobile/lib/common/domain/model.dart @@ -122,6 +122,13 @@ class Usd { return usd < other.usd; } + @override + bool operator ==(Object other) => + other is Usd && other.runtimeType == runtimeType && other._usd == _usd; + + @override + int get hashCode => _usd.hashCode; + Usd.parseString(String? value) { if (value == null || value.isEmpty) { _usd = Decimal.zero; diff --git a/mobile/lib/features/trade/domain/trade_values.dart b/mobile/lib/features/trade/domain/trade_values.dart index b728b3837..f11555cad 100644 --- a/mobile/lib/features/trade/domain/trade_values.dart +++ b/mobile/lib/features/trade/domain/trade_values.dart @@ -5,6 +5,10 @@ import 'package:get_10101/features/trade/domain/leverage.dart'; class TradeValues { /// Potential quantity already in an open position + /// + /// Note the open quantity is only set for the opposite direction. + /// So if you'd go 100 long the open quantity would be 0 for the long direction and 100 for the + /// short direction. Usd _openQuantity = Usd.zero(); get openQuantity => _openQuantity; @@ -58,10 +62,8 @@ class TradeValues { Amount? margin = tradeValuesService.calculateMargin(price: price, quantity: quantity, leverage: leverage); - double? liquidationPrice = price != null - ? tradeValuesService.calculateLiquidationPrice( - price: price, leverage: leverage, direction: direction) - : null; + double? liquidationPrice = tradeValuesService.calculateLiquidationPrice( + price: price, leverage: leverage, direction: direction); Amount? fee = tradeValuesService.orderMatchingFee(quantity: quantity, price: price); @@ -88,6 +90,7 @@ class TradeValues { this.contracts = contracts; _recalculateMargin(); _recalculateFee(); + _recalculateLiquidationPrice(); } updateMargin(Amount margin) { @@ -139,9 +142,16 @@ class TradeValues { } _recalculateLiquidationPrice() { - double? liquidationPrice = tradeValuesService.calculateLiquidationPrice( - price: price, leverage: leverage, direction: direction); - this.liquidationPrice = liquidationPrice; + if (quantity.usd == 0) { + // the user is only reducing his position hence we need to calculate the liquidation price based on the opposite direction. + double? liquidationPrice = tradeValuesService.calculateLiquidationPrice( + price: price, leverage: leverage, direction: direction.opposite()); + this.liquidationPrice = liquidationPrice; + } else { + double? liquidationPrice = tradeValuesService.calculateLiquidationPrice( + price: price, leverage: leverage, direction: direction); + this.liquidationPrice = liquidationPrice; + } } _recalculateFee() { diff --git a/mobile/lib/features/trade/trade_bottom_sheet_confirmation.dart b/mobile/lib/features/trade/trade_bottom_sheet_confirmation.dart index 93461fb6b..8ae6b830a 100644 --- a/mobile/lib/features/trade/trade_bottom_sheet_confirmation.dart +++ b/mobile/lib/features/trade/trade_bottom_sheet_confirmation.dart @@ -19,8 +19,19 @@ import 'package:provider/provider.dart'; import 'package:slide_to_confirm/slide_to_confirm.dart'; enum TradeAction { + /// No channel exists. openChannel, + + /// Open quantity is bigger than the contracts. The user is partially closing their position. + reducePosition, + + /// The open quantity is smaller than the contracts. The user is changing directions. + changeDirection, + + /// The user is either extending or opening a new position. (or changing direction) trade, + + /// Open quantity is exactly the amount of contracts. The user is closing their position. closePosition, } @@ -76,7 +87,7 @@ tradeBottomSheetConfirmation( }, child: SingleChildScrollView( child: SizedBox( - height: TradeAction.closePosition == tradeAction ? 330 : 500, + height: TradeAction.closePosition == tradeAction ? 380 : 500, child: TradeBottomSheetConfirmation( direction: direction, sliderButtonKey: sliderButtonKey, @@ -95,30 +106,6 @@ tradeBottomSheetConfirmation( ); } -// TODO: Include slider/button too. -RichText confirmationText(BuildContext context, TradeAction tradeAction, Amount total) { - switch (tradeAction) { - case TradeAction.closePosition: - return RichText( - text: TextSpan( - text: - '\nBy confirming, a closing market order will be created. Once the order is matched, your position will be closed.', - style: DefaultTextStyle.of(context).style)); - case TradeAction.openChannel: - case TradeAction.trade: - return RichText( - text: TextSpan( - text: '\nBy confirming, a new order will be created. Once the order is matched, ', - style: DefaultTextStyle.of(context).style, - children: [ - TextSpan(text: formatSats(total), style: const TextStyle(fontWeight: FontWeight.bold)), - const TextSpan(text: ' will be locked up in a DLC channel!'), - ], - ), - ); - } -} - class TradeBottomSheetConfirmation extends StatelessWidget { final Direction direction; final Key sliderKey; @@ -154,6 +141,8 @@ class TradeBottomSheetConfirmation extends StatelessWidget { bool isClose = tradeAction == TradeAction.closePosition; bool isChannelOpen = tradeAction == TradeAction.openChannel; + bool isReduce = tradeAction == TradeAction.reducePosition; + bool isChangedDirection = tradeAction == TradeAction.changeDirection; final traderCollateral1 = traderCollateral ?? Amount.zero(); @@ -180,6 +169,52 @@ class TradeBottomSheetConfirmation extends StatelessWidget { ? Amount((referralStatus.referralFeeBonus * orderMatchingFee.sats).floor()) : Amount.zero(); + final description = switch (tradeAction) { + TradeAction.closePosition => RichText( + text: TextSpan( + text: + '\nBy confirming, a market order will be created. Once the order is matched, your position will be closed.', + style: DefaultTextStyle.of(context).style)), + TradeAction.openChannel || TradeAction.trade => RichText( + text: TextSpan( + text: '\nBy confirming, a market order will be created. Once the order is matched, ', + style: DefaultTextStyle.of(context).style, + children: [ + TextSpan( + text: formatSats(total), style: const TextStyle(fontWeight: FontWeight.bold)), + const TextSpan(text: ' will be locked up in a DLC channel!'), + ], + ), + ), + TradeAction.reducePosition => RichText( + text: TextSpan( + text: '\nBy confirming, a market order will be created reducing your position to ', + style: DefaultTextStyle.of(context).style, + children: [ + TextSpan( + text: formatUsd(tradeValues.openQuantity - tradeValues.contracts), + style: const TextStyle(fontWeight: FontWeight.bold)), + ], + ), + ), + TradeAction.changeDirection => RichText( + text: TextSpan( + text: + '\nBy confirming, a market order will be created changing the direction of your position to ', + style: DefaultTextStyle.of(context).style, + children: [ + TextSpan( + text: formatUsd(tradeValues.contracts - tradeValues.openQuantity), + style: const TextStyle(fontWeight: FontWeight.bold)), + TextSpan(text: ' ${tradeValues.direction.nameU}.\n\nOnce the order is matched, '), + TextSpan( + text: formatSats(total), style: const TextStyle(fontWeight: FontWeight.bold)), + const TextSpan(text: ' will be locked up in a DLC channel!') + ], + ), + ), + }; + return Container( padding: EdgeInsets.only(left: 20, right: 20, top: (isClose ? 20 : 10), bottom: 10), child: Column( @@ -195,31 +230,37 @@ class TradeBottomSheetConfirmation extends StatelessWidget { Wrap( runSpacing: 5, children: [ + ValueDataRow( + type: ValueType.fiat, + value: tradeValues.contracts.asDouble(), + label: "Quantity"), if (!isClose) ValueDataRow( type: ValueType.date, value: tradeValues.expiry.toLocal(), label: 'Expiry'), - isClose - ? ValueDataRow( - type: ValueType.fiat, - value: tradeValues.price ?? 0.0, - label: 'Market Price') - : ValueDataRow( - type: ValueType.amount, value: tradeValues.margin, label: 'Margin'), - isClose - ? ValueDataRow( - type: ValueType.amount, - value: pnl, - label: 'Unrealized P/L', - valueTextStyle: dataRowStyle.apply( - color: - pnl.sats.isNegative ? tradeTheme.loss : tradeTheme.profit)) - : ValueDataRow( - type: ValueType.fiat, - value: tradeValues.liquidationPrice ?? 0.0, - label: 'Liquidation Price', - ), + if (isClose) + ValueDataRow( + type: ValueType.fiat, + value: tradeValues.price ?? 0.0, + label: 'Market Price'), + if (!isReduce) + ValueDataRow( + type: ValueType.amount, value: tradeValues.margin, label: 'Margin'), + if (isReduce || isClose || isChangedDirection) + ValueDataRow( + type: ValueType.amount, + value: pnl, + label: 'Unrealized P/L', + valueTextStyle: dataRowStyle.apply( + color: + pnl.sats.isNegative ? tradeTheme.loss : tradeTheme.profit)), + if (!isClose) + ValueDataRow( + type: ValueType.fiat, + value: tradeValues.liquidationPrice ?? 0.0, + label: 'Liquidation Price', + ), ValueDataRow( type: ValueType.amount, value: orderMatchingFee, @@ -266,15 +307,15 @@ class TradeBottomSheetConfirmation extends StatelessWidget { : const SizedBox(height: 0), ], ), - !isClose ? const Divider() : const SizedBox(height: 0), - !isClose + !isClose && !isReduce ? const Divider() : const SizedBox(height: 0), + !isClose && !isReduce ? ValueDataRow(type: ValueType.amount, value: total, label: "Total") : const SizedBox(height: 0), ], ), ), ), - confirmationText(context, tradeAction, total), + description, const Spacer(), ConfirmationSlider( key: sliderKey, diff --git a/mobile/lib/features/trade/trade_bottom_sheet_tab.dart b/mobile/lib/features/trade/trade_bottom_sheet_tab.dart index 64f2d7bec..98ad32005 100644 --- a/mobile/lib/features/trade/trade_bottom_sheet_tab.dart +++ b/mobile/lib/features/trade/trade_bottom_sheet_tab.dart @@ -149,7 +149,7 @@ class _TradeBottomSheetTabState extends State if (_formKey.currentState!.validate()) { final submitOrderChangeNotifier = context.read(); - final tradeAction = hasChannel ? TradeAction.trade : TradeAction.openChannel; + final tradeAction = getTradeAction(tradeValues, hasChannel); switch (tradeAction) { case TradeAction.openChannel: @@ -173,7 +173,9 @@ class _TradeBottomSheetTabState extends State break; } case TradeAction.trade: + case TradeAction.reducePosition: case TradeAction.closePosition: + case TradeAction.changeDirection: tradeBottomSheetConfirmation( context: context, direction: direction, @@ -351,8 +353,13 @@ class _TradeBottomSheetTabState extends State selector: (_, provider) => provider.fromDirection(direction).liquidationPrice ?? 0.0, builder: (context, liquidationPrice, child) { - return ValueDataRow( - type: ValueType.fiat, value: liquidationPrice, label: "Liquidation:"); + if (tradeValues.openQuantity == tradeValues.contracts) { + // the position would be closed at this quantity. It does not make sense to show the liquidation price. + return const SizedBox(width: 135, child: Text('Liquidation: n/a')); + } else { + return ValueDataRow( + type: ValueType.fiat, value: liquidationPrice, label: "Liquidation:"); + } }), const SizedBox(width: 55), Selector( @@ -391,4 +398,26 @@ class _TradeBottomSheetTabState extends State @override bool get wantKeepAlive => true; + + /// Returns the trade action depending on the trade values and if a channel exists + TradeAction getTradeAction(TradeValues tradeValues, bool hasChannel) { + if (!hasChannel) { + return TradeAction.openChannel; + } + + if (tradeValues.openQuantity == tradeValues.contracts) { + return TradeAction.closePosition; + } + + if (tradeValues.openQuantity > tradeValues.contracts) { + return TradeAction.reducePosition; + } + + if (tradeValues.openQuantity != Usd.zero() && + tradeValues.openQuantity < tradeValues.contracts) { + return TradeAction.changeDirection; + } + + return TradeAction.trade; + } }