Skip to content

Commit

Permalink
Merge pull request #2439 from get10101/fix/max-quantity-with-resizing
Browse files Browse the repository at this point in the history
fix: Calculate max quantity for resizing
  • Loading branch information
holzeis authored Apr 18, 2024
2 parents 2fe6fde + d77d4b8 commit 40ef537
Show file tree
Hide file tree
Showing 25 changed files with 429 additions and 166 deletions.
24 changes: 24 additions & 0 deletions mobile/lib/common/domain/model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ class Amount {
return Amount(sats * other.sats);
}

bool operator >(Amount other) {
return sats > other.sats;
}

Amount.parseAmount(String? value) {
if (value == null || value.isEmpty) {
_sats = Decimal.zero;
Expand Down Expand Up @@ -98,6 +102,26 @@ class Usd {

Usd.parse(dynamic value) : _usd = Decimal.parse(value);

Usd operator +(Usd other) {
return Usd(usd + other.usd);
}

Usd operator -(Usd other) {
return Usd(usd - other.usd);
}

bool operator >(Usd other) {
return usd > other.usd;
}

bool operator >=(Usd other) {
return usd >= other.usd;
}

bool operator <(Usd other) {
return usd < other.usd;
}

Usd.parseString(String? value) {
if (value == null || value.isEmpty) {
_usd = Decimal.zero;
Expand Down
6 changes: 0 additions & 6 deletions mobile/lib/common/dummy_values.dart

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,13 @@ class TradeValuesService {
}
}

Usd? calculateMaxQuantity({required double? price, required Leverage leverage}) {
Usd? calculateMaxQuantity(
{required double? price, required Leverage leverage, required Direction direction}) {
if (price == null) {
return null;
} else {
final quantity =
rust.api.calculateMaxQuantity(price: price, traderLeverage: leverage.leverage);
final quantity = rust.api.calculateMaxQuantity(
price: price, traderLeverage: leverage.leverage, traderDirection: direction.toApi());

return Usd(quantity);
}
Expand Down
4 changes: 2 additions & 2 deletions mobile/lib/features/trade/channel_configuration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,9 @@ class _ChannelConfiguration extends State<ChannelConfiguration> {
DlcChannelService dlcChannelService =
context.read<DlcChannelChangeNotifier>().dlcChannelService;

maxCounterpartyCollateral = Amount(tradeConstraints.maxCounterpartyMarginSats);
maxCounterpartyCollateral = Amount(tradeConstraints.maxCounterpartyBalanceSats);

maxOnChainSpending = Amount(tradeConstraints.maxLocalMarginSats);
maxOnChainSpending = Amount(tradeConstraints.maxLocalBalanceSats);
counterpartyLeverage = tradeConstraints.coordinatorLeverage;

counterpartyMargin = widget.tradeValues.calculateMargin(Leverage(counterpartyLeverage));
Expand Down
71 changes: 29 additions & 42 deletions mobile/lib/features/trade/domain/trade_values.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,33 @@ import 'package:get_10101/features/trade/domain/direction.dart';
import 'package:get_10101/features/trade/domain/leverage.dart';

class TradeValues {
/// Potential quantity already in an open position
Usd _openQuantity = Usd.zero();

get openQuantity => _openQuantity;

set openQuantity(quantity) => _openQuantity = quantity;

/// The difference between open quantity and quantity if contracts is bigger than the open quantity.
/// This value is used to calculate the required margin.
Usd quantity = Usd.zero();

/// The actual contracts entered. Any value between 0 and maxQuantity.
Usd contracts = Usd.zero();

Amount? margin;
Leverage leverage;
Direction direction;

// These values can be null if coordinator is down
Usd? quantity;
double? price;
double? liquidationPrice;
Amount? fee; // This fee is an estimate of the order-matching fee.
Usd? maxQuantity;

double fundingRate;
Usd? _maxQuantity;

/// The max quantity of the contracts not part of the open quantity.
Usd get maxQuantity => _maxQuantity ?? Usd.zero();

DateTime expiry;

// no final so it can be mocked in tests
Expand All @@ -29,7 +44,6 @@ class TradeValues {
required this.price,
required this.liquidationPrice,
required this.fee,
required this.fundingRate,
required this.expiry,
required this.tradeValuesService,
});
Expand All @@ -38,44 +52,12 @@ class TradeValues {
required Usd quantity,
required Leverage leverage,
required double? price,
required double fundingRate,
required Direction direction,
required TradeValuesService tradeValuesService,
}) {
Amount? margin =
tradeValuesService.calculateMargin(price: price, quantity: quantity, leverage: leverage);
double? liquidationPrice = price != null
? tradeValuesService.calculateLiquidationPrice(
price: price, leverage: leverage, direction: direction)
: null;

Amount? fee = tradeValuesService.orderMatchingFee(quantity: quantity, price: price);

DateTime expiry = tradeValuesService.getExpiryTimestamp();

return TradeValues(
direction: direction,
margin: margin,
quantity: quantity,
leverage: leverage,
price: price,
fundingRate: fundingRate,
liquidationPrice: liquidationPrice,
fee: fee,
expiry: expiry,
tradeValuesService: tradeValuesService);
}

factory TradeValues.fromMargin({
required Amount? margin,
required Leverage leverage,
required double? price,
required double fundingRate,
required Direction direction,
required TradeValuesService tradeValuesService,
}) {
Usd? quantity =
tradeValuesService.calculateQuantity(price: price, margin: margin, leverage: leverage);
double? liquidationPrice = price != null
? tradeValuesService.calculateLiquidationPrice(
price: price, leverage: leverage, direction: direction)
Expand All @@ -91,7 +73,6 @@ class TradeValues {
quantity: quantity,
leverage: leverage,
price: price,
fundingRate: fundingRate,
liquidationPrice: liquidationPrice,
fee: fee,
expiry: expiry,
Expand All @@ -101,6 +82,11 @@ class TradeValues {
updateQuantity(Usd quantity) {
this.quantity = quantity;
_recalculateMargin();
}

updateContracts(Usd contracts) {
this.contracts = contracts;
_recalculateMargin();
_recalculateFee();
}

Expand Down Expand Up @@ -149,7 +135,7 @@ class TradeValues {
_recalculateQuantity() {
Usd? quantity =
tradeValuesService.calculateQuantity(price: price, margin: margin, leverage: leverage);
this.quantity = quantity;
this.quantity = quantity ?? Usd.zero();
}

_recalculateLiquidationPrice() {
Expand All @@ -159,13 +145,14 @@ class TradeValues {
}

_recalculateFee() {
fee = tradeValuesService.orderMatchingFee(quantity: quantity, price: price);
fee = tradeValuesService.orderMatchingFee(quantity: contracts, price: price);
}

recalculateMaxQuantity() {
final quantity = tradeValuesService.calculateMaxQuantity(price: price, leverage: leverage);
final quantity = tradeValuesService.calculateMaxQuantity(
price: price, leverage: leverage, direction: direction);
if (quantity != null) {
maxQuantity = quantity;
_maxQuantity = quantity;
}
}
}
34 changes: 15 additions & 19 deletions mobile/lib/features/trade/submit_order_change_notifier.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,6 @@ class SubmitOrderChangeNotifier extends ChangeNotifier implements Subscriber {
notifyListeners();

try {
assert(tradeValues.quantity != null, 'Quantity cannot be null when submitting order');

if (channelOpeningParams != null) {
// TODO(holzeis): The coordinator leverage should not be hard coded here.
final coordinatorCollateral = tradeValues.calculateMargin(Leverage(2.0));
Expand All @@ -70,15 +68,15 @@ class SubmitOrderChangeNotifier extends ChangeNotifier implements Subscriber {

_pendingOrder!.id = await orderService.submitChannelOpeningMarketOrder(
tradeValues.leverage,
tradeValues.quantity!,
tradeValues.contracts,
ContractSymbol.btcusd,
tradeValues.direction,
stable,
Amount(coordinatorReserve),
Amount(traderReserve));
} else {
_pendingOrder!.id = await orderService.submitMarketOrder(tradeValues.leverage,
tradeValues.quantity!, ContractSymbol.btcusd, tradeValues.direction, stable);
tradeValues.contracts, ContractSymbol.btcusd, tradeValues.direction, stable);
}

_pendingOrder!.state = PendingOrderState.submittedSuccessfully;
Expand Down Expand Up @@ -121,21 +119,19 @@ class SubmitOrderChangeNotifier extends ChangeNotifier implements Subscriber {

Future<void> closePosition(Position position, double? closingPrice, Amount? fee,
{bool stable = false}) async {
await submitPendingOrder(
TradeValues(
direction: position.direction.opposite(),
margin: position.collateral,
quantity: position.quantity,
leverage: position.leverage,
price: closingPrice,
liquidationPrice: position.liquidationPrice,
fee: fee,
fundingRate: 0,
expiry: position.expiry,
tradeValuesService: const TradeValuesService()),
PositionAction.close,
pnl: position.unrealizedPnl,
stable: stable);
final tradeValues = TradeValues(
direction: position.direction.opposite(),
margin: position.collateral,
quantity: position.quantity,
leverage: position.leverage,
price: closingPrice,
liquidationPrice: position.liquidationPrice,
fee: fee,
expiry: position.expiry,
tradeValuesService: const TradeValuesService());
tradeValues.contracts = position.quantity;
await submitPendingOrder(tradeValues, PositionAction.close,
pnl: position.unrealizedPnl, stable: stable);
}

PendingOrder? get pendingOrder => _pendingOrder;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,6 @@ class TradeBottomSheetConfirmation extends StatelessWidget {
// Fallback to 0 if we can't get the fee or the margin
Amount total =
tradeValues.margin != null ? Amount(tradeValues.margin!.sats).add(reserve) : Amount(0);
total = total.add(tradeValues.fee ?? Amount.zero());

Amount pnl = Amount(0);
if (context.read<PositionChangeNotifier>().positions.containsKey(ContractSymbol.btcusd)) {
Expand Down
60 changes: 50 additions & 10 deletions mobile/lib/features/trade/trade_bottom_sheet_tab.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ class TradeBottomSheetTab extends StatefulWidget {
State<TradeBottomSheetTab> createState() => _TradeBottomSheetTabState();
}

class _TradeBottomSheetTabState extends State<TradeBottomSheetTab> {
class _TradeBottomSheetTabState extends State<TradeBottomSheetTab>
with AutomaticKeepAliveClientMixin<TradeBottomSheetTab> {
late final TradeValuesChangeNotifier provider;
late final TenTenOneConfigChangeNotifier tentenoneConfigChangeNotifier;
late final PositionChangeNotifier positionChangeNotifier;
Expand All @@ -59,6 +60,39 @@ class _TradeBottomSheetTabState extends State<TradeBottomSheetTab> {
tentenoneConfigChangeNotifier = context.read<TenTenOneConfigChangeNotifier>();
positionChangeNotifier = context.read<PositionChangeNotifier>();

// init the short trade values
final shortTradeValues = provider.fromDirection(Direction.short);
shortTradeValues.updateQuantity(shortTradeValues.maxQuantity);
// overwrite any potential pre-existing state
shortTradeValues.openQuantity = Usd.zero();

// by default we set the amount to the max quantity.
shortTradeValues.updateContracts(shortTradeValues.maxQuantity);

// init the long trade values
final longTradeValues = provider.fromDirection(Direction.long);
longTradeValues.updateQuantity(longTradeValues.maxQuantity);
// overwrite any potential pre-existing state
longTradeValues.openQuantity = Usd.zero();

// by default we set the amount to the max quantity.
longTradeValues.updateContracts(longTradeValues.maxQuantity);

if (positionChangeNotifier.positions.containsKey(contractSymbol)) {
// in case there is an open position we have to set the open quantity for the trade values of
// the opposite direction
final position = positionChangeNotifier.positions[contractSymbol]!;
final tradeValues = provider.fromDirection(position.direction.opposite());

tradeValues.openQuantity = position.quantity;
tradeValues.updateQuantity(tradeValues.maxQuantity - tradeValues.openQuantity);

// by default the contracts are set to the amount of open contracts of the current position.
tradeValues.updateContracts(tradeValues.openQuantity);
}

provider.maxQuantityLock = false;

super.initState();
}

Expand All @@ -73,6 +107,8 @@ class _TradeBottomSheetTabState extends State<TradeBottomSheetTab> {

@override
Widget build(BuildContext context) {
super.build(context);

TradeTheme tradeTheme = Theme.of(context).extension<TradeTheme>()!;
DlcChannelChangeNotifier dlcChannelChangeNotifier = context.watch<DlcChannelChangeNotifier>();

Expand Down Expand Up @@ -189,14 +225,16 @@ class _TradeBottomSheetTabState extends State<TradeBottomSheetTab> {
bool hasPosition = positionChangeNotifier.positions.containsKey(contractSymbol);

double? positionLeverage;
int usableBalance = channelTradeConstraints.maxLocalBalanceSats;
if (hasPosition) {
final position = context.read<PositionChangeNotifier>().positions[contractSymbol];
positionLeverage = position!.leverage.leverage;
if (direction == position.direction.opposite()) {
usableBalance += ((position.unrealizedPnl ?? Amount.zero()) + position.collateral).sats;
}
}

int usableBalance = channelTradeConstraints.maxLocalMarginSats;

quantityController.text = Amount(tradeValues.quantity?.toInt ?? 0).formatted();
quantityController.text = Amount(tradeValues.contracts.usd).formatted();

return Wrap(
runSpacing: 12,
Expand Down Expand Up @@ -227,8 +265,7 @@ class _TradeBottomSheetTabState extends State<TradeBottomSheetTab> {
controller: quantityController,
suffixIcon: TextButton(
onPressed: () {
final quantity = tradeValues.maxQuantity ?? Usd.zero();
quantityController.text = quantity.formatted();
final quantity = tradeValues.maxQuantity;
setState(() {
provider.maxQuantityLock = !provider.maxQuantityLock;
context.read<TradeValuesChangeNotifier>().updateQuantity(direction, quantity);
Expand Down Expand Up @@ -264,15 +301,15 @@ class _TradeBottomSheetTabState extends State<TradeBottomSheetTab> {
_formKey.currentState?.validate();
},
validator: (value) {
Amount quantity = Amount.parseAmount(value);
Usd quantity = Usd.parseString(value);

if (quantity.toInt < channelTradeConstraints.minQuantity) {
return "Min quantity is ${channelTradeConstraints.minQuantity}";
}

final maxQuantity = tradeValues.maxQuantity?.toInt ?? 0;
if (quantity.toInt > maxQuantity) {
return "Max quantity is ${maxQuantity.toInt()}";
final maxQuantity = tradeValues.maxQuantity + tradeValues.openQuantity;
if (quantity > maxQuantity) {
return "Max quantity is $maxQuantity";
}

return null;
Expand Down Expand Up @@ -350,4 +387,7 @@ class _TradeBottomSheetTabState extends State<TradeBottomSheetTab> {
],
);
}

@override
bool get wantKeepAlive => true;
}
Loading

0 comments on commit 40ef537

Please sign in to comment.