Skip to content

Commit

Permalink
Merge pull request #2332 from get10101/feat/max-trade-button
Browse files Browse the repository at this point in the history
feat: Add max quantity button
  • Loading branch information
holzeis authored Apr 1, 2024
2 parents 5309ed5 + a0dc14b commit 02c7627
Show file tree
Hide file tree
Showing 12 changed files with 507 additions and 90 deletions.
6 changes: 3 additions & 3 deletions mobile/lib/common/amount_text_input_form_field.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class AmountInputField extends StatelessWidget {
this.label = '',
this.hint = '',
this.onChanged,
required this.initialValue,
this.initialValue,
this.isLoading = false,
this.suffixIcon,
this.controller,
Expand All @@ -23,7 +23,7 @@ class AmountInputField extends StatelessWidget {

final TextEditingController? controller;
final TextStyle? style;
final Amount initialValue;
final Amount? initialValue;
final bool enabled;
final String label;
final String hint;
Expand All @@ -41,7 +41,7 @@ class AmountInputField extends StatelessWidget {
style: style ?? const TextStyle(color: Colors.black87, fontSize: 16),
enabled: enabled,
controller: controller,
initialValue: controller != null ? null : initialValue.formatted(),
initialValue: controller != null ? null : initialValue?.formatted(),
keyboardType: TextInputType.number,
decoration: decoration ??
InputDecoration(
Expand Down
8 changes: 4 additions & 4 deletions mobile/lib/common/init_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ import 'package:get_10101/bridge_generated/bridge_definitions.dart' as bridge;
List<SingleChildWidget> createProviders() {
bridge.Config config = Environment.parse();

const ChannelInfoService channelInfoService = ChannelInfoService();
var tradeValuesService = TradeValuesService();
const tradeValuesService = TradeValuesService();
const channelInfoService = ChannelInfoService();
const dlcChannelService = DlcChannelService();
const pollService = PollService();
const githubService = GitHubService();

Expand All @@ -59,8 +60,7 @@ List<SingleChildWidget> createProviders() {
ChangeNotifierProvider(
create: (context) => CandlestickChangeNotifier(const CandlestickService()).initialize()),
ChangeNotifierProvider(create: (context) => ServiceStatusNotifier()),
ChangeNotifierProvider(
create: (context) => DlcChannelChangeNotifier(const DlcChannelService())),
ChangeNotifierProvider(create: (context) => DlcChannelChangeNotifier(dlcChannelService)),
ChangeNotifierProvider(create: (context) => AsyncOrderChangeNotifier(OrderService())),
ChangeNotifierProvider(create: (context) => RolloverChangeNotifier()),
ChangeNotifierProvider(create: (context) => RecoverDlcChangeNotifier()),
Expand Down
15 changes: 14 additions & 1 deletion mobile/lib/features/trade/application/trade_values_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import 'package:get_10101/features/trade/domain/leverage.dart';
import 'package:get_10101/ffi.dart' as rust;

class TradeValuesService {
const TradeValuesService();

Amount? calculateMargin(
{required double? price, required Usd? quantity, required Leverage leverage, dynamic hint}) {
if (price == null || quantity == null) {
Expand All @@ -21,7 +23,18 @@ class TradeValuesService {
} else {
final quantity = rust.api
.calculateQuantity(price: price, margin: margin.sats, leverage: leverage.leverage);
return Usd(quantity.ceil());
return Usd(quantity.floor());
}
}

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

return Usd(quantity);
}
}

Expand Down
64 changes: 38 additions & 26 deletions mobile/lib/features/trade/domain/trade_values.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,32 +13,35 @@ class TradeValues {
double? price;
double? liquidationPrice;
Amount? fee; // This fee is an estimate of the order-matching fee.
Usd? maxQuantity;

double fundingRate;
DateTime expiry;

// no final so it can be mocked in tests
TradeValuesService tradeValuesService;

TradeValues(
{required this.direction,
required this.margin,
required this.quantity,
required this.leverage,
required this.price,
required this.liquidationPrice,
required this.fee,
required this.fundingRate,
required this.expiry,
required this.tradeValuesService});

factory TradeValues.fromQuantity(
{required Usd quantity,
required Leverage leverage,
required double? price,
required double fundingRate,
required Direction direction,
required TradeValuesService tradeValuesService}) {
TradeValues({
required this.direction,
required this.margin,
required this.quantity,
required this.leverage,
required this.price,
required this.liquidationPrice,
required this.fee,
required this.fundingRate,
required this.expiry,
required this.tradeValuesService,
});

factory TradeValues.fromQuantity({
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
Expand All @@ -63,13 +66,14 @@ class TradeValues {
tradeValuesService: tradeValuesService);
}

factory TradeValues.fromMargin(
{required Amount? margin,
required Leverage leverage,
required double? price,
required double fundingRate,
required Direction direction,
required 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
Expand Down Expand Up @@ -104,26 +108,30 @@ class TradeValues {
this.margin = margin;
_recalculateQuantity();
_recalculateFee();
_recalculateMaxQuantity();
}

updatePriceAndQuantity(double? price) {
this.price = price;
_recalculateQuantity();
_recalculateLiquidationPrice();
_recalculateFee();
_recalculateMaxQuantity();
}

updatePriceAndMargin(double? price) {
this.price = price;
_recalculateMargin();
_recalculateLiquidationPrice();
_recalculateFee();
_recalculateMaxQuantity();
}

updateLeverage(Leverage leverage) {
this.leverage = leverage;
_recalculateMargin();
_recalculateLiquidationPrice();
_recalculateMaxQuantity();
}

// Can be used to calculate the counterparty's margin, based on their
Expand Down Expand Up @@ -153,4 +161,8 @@ class TradeValues {
_recalculateFee() {
fee = tradeValuesService.orderMatchingFee(quantity: quantity, price: price);
}

_recalculateMaxQuantity() {
maxQuantity = tradeValuesService.calculateMaxQuantity(price: price, leverage: leverage);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ class SubmitOrderChangeNotifier extends ChangeNotifier implements Subscriber {
fee: fee,
fundingRate: 0,
expiry: position.expiry,
tradeValuesService: TradeValuesService()),
tradeValuesService: const TradeValuesService()),
PositionAction.close,
pnl: position.unrealizedPnl,
stable: stable);
Expand Down
73 changes: 29 additions & 44 deletions mobile/lib/features/trade/trade_bottom_sheet_tab.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:get_10101/common/amount_text_field.dart';
import 'package:get_10101/common/amount_text_input_form_field.dart';
import 'package:get_10101/common/application/channel_info_service.dart';
import 'package:get_10101/common/application/lsp_change_notifier.dart';
import 'package:get_10101/common/color.dart';
import 'package:get_10101/common/dlc_channel_change_notifier.dart';
import 'package:get_10101/common/domain/model.dart';
import 'package:get_10101/features/trade/channel_configuration.dart';
Expand Down Expand Up @@ -47,8 +48,6 @@ class _TradeBottomSheetTabState extends State<TradeBottomSheetTab> {

final _formKey = GlobalKey<FormState>();

bool showCapacityInfo = false;

bool marginInputFieldEnabled = false;
bool quantityInputFieldEnabled = true;

Expand Down Expand Up @@ -183,7 +182,7 @@ class _TradeBottomSheetTabState extends State<TradeBottomSheetTab> {

Wrap buildChildren(Direction direction, rust.TradeConstraints channelTradeConstraints,
BuildContext context, ChannelInfoService channelInfoService, GlobalKey<FormState> formKey) {
final tradeValues = context.read<TradeValuesChangeNotifier>().fromDirection(direction);
final tradeValues = context.watch<TradeValuesChangeNotifier>().fromDirection(direction);

bool hasPosition = positionChangeNotifier.positions.containsKey(contractSymbol);

Expand All @@ -195,11 +194,7 @@ class _TradeBottomSheetTabState extends State<TradeBottomSheetTab> {

int usableBalance = channelTradeConstraints.maxLocalMarginSats;

// We compute the max quantity based on the margin needed for the counterparty and how much he has available.
double price = tradeValues.price ?? 0.0;
double maxQuantity = (channelTradeConstraints.maxCounterpartyMarginSats / 100000000) *
price *
channelTradeConstraints.coordinatorLeverage;
quantityController.text = Amount(tradeValues.quantity?.toInt ?? 0).formatted();

return Wrap(
runSpacing: 12,
Expand Down Expand Up @@ -227,7 +222,29 @@ class _TradeBottomSheetTabState extends State<TradeBottomSheetTab> {
children: [
Flexible(
child: AmountInputField(
initialValue: Amount(tradeValues.quantity?.toInt ?? 0),
controller: quantityController,
suffixIcon: TextButton(
onPressed: () {
final quantity = tradeValues.maxQuantity ?? Usd.zero();
quantityController.text = quantity.formatted();
setState(() {
provider.maxQuantityLock = !provider.maxQuantityLock;
context.read<TradeValuesChangeNotifier>().updateQuantity(direction, quantity);
});
_formKey.currentState?.validate();
},
child: Container(
padding: const EdgeInsets.all(5.0),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(10)),
color:
provider.maxQuantityLock ? tenTenOnePurple.shade50 : Colors.transparent),
child: const Text(
"Max",
style: TextStyle(fontWeight: FontWeight.bold),
),
),
),
hint: "e.g. 100 USD",
label: "Quantity (USD)",
onChanged: (value) {
Expand All @@ -241,6 +258,7 @@ class _TradeBottomSheetTabState extends State<TradeBottomSheetTab> {
} on Exception {
context.read<TradeValuesChangeNotifier>().updateQuantity(direction, Usd.zero());
}
provider.maxQuantityLock = false;
_formKey.currentState?.validate();
},
validator: (value) {
Expand All @@ -250,43 +268,11 @@ class _TradeBottomSheetTabState extends State<TradeBottomSheetTab> {
return "Min quantity is ${channelTradeConstraints.minQuantity}";
}

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

double coordinatorLeverage = channelTradeConstraints.coordinatorLeverage;

int? optCounterPartyMargin =
provider.counterpartyMargin(direction, coordinatorLeverage);
if (optCounterPartyMargin == null) {
return "Counterparty margin not available";
}
int neededCounterpartyMarginSats = optCounterPartyMargin;

// This condition has to stay as the first thing to check, so we reset showing the info
int maxCounterpartyMarginSats = channelTradeConstraints.maxCounterpartyMarginSats;
int maxLocalMarginSats = channelTradeConstraints.maxLocalMarginSats;

// First we check if we have enough money, then we check if counterparty would have enough money
Amount fee = provider.orderMatchingFee(direction) ?? Amount.zero();

Amount margin = tradeValues.margin!;
int neededLocalMarginSats = margin.sats + fee.sats;

if (neededLocalMarginSats > maxLocalMarginSats) {
setState(() => showCapacityInfo = true);
return "Insufficient balance";
}

if (neededCounterpartyMarginSats > maxCounterpartyMarginSats) {
setState(() => showCapacityInfo = true);
return "Counterparty has insufficient balance";
}

setState(() {
showCapacityInfo = false;
});
return null;
},
)),
Expand Down Expand Up @@ -315,8 +301,7 @@ class _TradeBottomSheetTabState extends State<TradeBottomSheetTab> {
isActive: !hasPosition,
onLeverageChanged: (value) {
context.read<TradeValuesChangeNotifier>().updateLeverage(direction, Leverage(value));
// When the slider changes, we validate the whole form.
formKey.currentState!.validate();
formKey.currentState?.validate();
}),
Row(
children: [
Expand Down
16 changes: 14 additions & 2 deletions mobile/lib/features/trade/trade_value_change_notifier.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ class TradeValuesChangeNotifier extends ChangeNotifier implements Subscriber {

Price? _price;

bool maxQuantityLock = false;

TradeValuesChangeNotifier(this.tradeValuesService) {
_buyTradeValues = _initOrder(Direction.long);
_sellTradeValues = _initOrder(Direction.short);
Expand Down Expand Up @@ -78,6 +80,7 @@ class TradeValuesChangeNotifier extends ChangeNotifier implements Subscriber {

void updateLeverage(Direction direction, Leverage leverage) {
fromDirection(direction).updateLeverage(leverage);
maxQuantityLock = false;
notifyListeners();
}

Expand All @@ -91,11 +94,20 @@ class TradeValuesChangeNotifier extends ChangeNotifier implements Subscriber {
bool update = false;

if (price.ask != _buyTradeValues.price) {
_buyTradeValues.updatePriceAndMargin(price.ask);
if (maxQuantityLock) {
_buyTradeValues.updatePriceAndQuantity(price.ask);
} else {
_buyTradeValues.updatePriceAndMargin(price.ask);
}

update = true;
}
if (price.bid != _sellTradeValues.price) {
_sellTradeValues.updatePriceAndMargin(price.bid);
if (maxQuantityLock) {
_sellTradeValues.updatePriceAndQuantity(price.bid);
} else {
_sellTradeValues.updatePriceAndMargin(price.bid);
}
update = true;
}
_price = price;
Expand Down
Loading

0 comments on commit 02c7627

Please sign in to comment.