Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Calculate liquidation price from opposite direction if the position is reduced #2459

Merged
merged 10 commits into from
Apr 23, 2024
7 changes: 7 additions & 0 deletions mobile/lib/common/domain/model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
24 changes: 17 additions & 7 deletions mobile/lib/features/trade/domain/trade_values.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand All @@ -88,6 +90,7 @@ class TradeValues {
this.contracts = contracts;
_recalculateMargin();
_recalculateFee();
_recalculateLiquidationPrice();
}

updateMargin(Amount margin) {
Expand Down Expand Up @@ -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() {
Expand Down
137 changes: 89 additions & 48 deletions mobile/lib/features/trade/trade_bottom_sheet_confirmation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

Expand Down Expand Up @@ -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,
Expand All @@ -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>[
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;
Expand Down Expand Up @@ -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();

Expand All @@ -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>[
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>[
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>[
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(
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
35 changes: 32 additions & 3 deletions mobile/lib/features/trade/trade_bottom_sheet_tab.dart
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ class _TradeBottomSheetTabState extends State<TradeBottomSheetTab>
if (_formKey.currentState!.validate()) {
final submitOrderChangeNotifier = context.read<SubmitOrderChangeNotifier>();

final tradeAction = hasChannel ? TradeAction.trade : TradeAction.openChannel;
final tradeAction = getTradeAction(tradeValues, hasChannel);

switch (tradeAction) {
case TradeAction.openChannel:
Expand All @@ -173,7 +173,9 @@ class _TradeBottomSheetTabState extends State<TradeBottomSheetTab>
break;
}
case TradeAction.trade:
case TradeAction.reducePosition:
case TradeAction.closePosition:
case TradeAction.changeDirection:
tradeBottomSheetConfirmation(
context: context,
direction: direction,
Expand Down Expand Up @@ -351,8 +353,13 @@ class _TradeBottomSheetTabState extends State<TradeBottomSheetTab>
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) {
holzeis marked this conversation as resolved.
Show resolved Hide resolved
// 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<TradeValuesChangeNotifier, Amount>(
Expand Down Expand Up @@ -391,4 +398,26 @@ class _TradeBottomSheetTabState extends State<TradeBottomSheetTab>

@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) {
holzeis marked this conversation as resolved.
Show resolved Hide resolved
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;
}
}
Loading