Skip to content

Commit

Permalink
Merge pull request #2459 from get10101/fix/liquidity-price-calculation
Browse files Browse the repository at this point in the history
fix: Calculate liquidation price from opposite direction if the position is reduced
  • Loading branch information
holzeis authored Apr 23, 2024
2 parents 834f2b8 + 2e5552d commit f3a221d
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 58 deletions.
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) {
// 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) {
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;
}
}

0 comments on commit f3a221d

Please sign in to comment.