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

feat(webapp): show orderhistory #1936

Merged
merged 2 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

- Feat(webapp): Show order history

## [1.8.4] - 2024-01-31

- Chore: Add funding txid to list dlc channels api
Expand Down
7 changes: 4 additions & 3 deletions mobile/native/src/trade/order/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::calculations::calculate_margin;
use crate::ln_dlc;
use rust_decimal::Decimal;
use serde::Serialize;
use time::OffsetDateTime;
use trade::ContractSymbol;
use trade::Direction;
Expand All @@ -13,14 +14,14 @@ mod orderbook_client;
// When naming this the same as `api_model::order::OrderType` the generated code somehow uses
// `trade::OrderType` and contains errors, hence different name is used.
// This is likely a bug in frb.
#[derive(Debug, Clone, Copy, PartialEq)]
#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
pub enum OrderType {
Market,
Limit { price: f32 },
}

/// Internal type so we still have Copy on order
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize)]
pub enum FailureReason {
/// An error occurred when setting the Order to filling in our DB
FailedToSetToFilling,
Expand All @@ -41,7 +42,7 @@ pub enum FailureReason {
Unknown,
}

#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, Serialize)]
pub enum InvalidSubchannelOffer {
/// Received offer was outdated
Outdated,
Expand Down
25 changes: 25 additions & 0 deletions webapp/frontend/lib/common/order_type.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
enum OrderType {
market,
limit;

String get asString {
switch (this) {
case OrderType.market:
return "Market";
case OrderType.limit:
return "Limit";
}
}

// Factory method to convert a String to OrderType
static OrderType fromString(String value) {
switch (value.toLowerCase()) {
case 'market':
return OrderType.market;
case 'limit':
return OrderType.limit;
default:
throw ArgumentError('Invalid OrderType: $value');
}
}
}
3 changes: 3 additions & 0 deletions webapp/frontend/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import 'package:get_10101/auth/auth_service.dart';
import 'package:get_10101/common/version_service.dart';
import 'package:get_10101/logger/logger.dart';
import 'package:get_10101/routes.dart';
import 'package:get_10101/trade/order_change_notifier.dart';
import 'package:get_10101/trade/order_service.dart';
import 'package:get_10101/trade/position_change_notifier.dart';
import 'package:get_10101/trade/position_service.dart';
import 'package:get_10101/trade/quote_change_notifier.dart';
Expand All @@ -25,6 +27,7 @@ void main() {
ChangeNotifierProvider(create: (context) => WalletChangeNotifier(const WalletService())),
ChangeNotifierProvider(create: (context) => QuoteChangeNotifier(const QuoteService())),
ChangeNotifierProvider(create: (context) => PositionChangeNotifier(const PositionService())),
ChangeNotifierProvider(create: (context) => OrderChangeNotifier(const OrderService())),
Provider(create: (context) => const SettingsService()),
Provider(create: (context) => AuthService())
];
Expand Down
283 changes: 5 additions & 278 deletions webapp/frontend/lib/trade/order_and_position_table.dart
Original file line number Diff line number Diff line change
@@ -1,18 +1,7 @@
import 'package:flutter/material.dart';
import 'package:get_10101/common/color.dart';
import 'package:get_10101/common/contract_symbol_icon.dart';
import 'package:get_10101/common/direction.dart';
import 'package:get_10101/common/model.dart';
import 'package:get_10101/common/snack_bar.dart';
import 'package:get_10101/common/theme.dart';
import 'package:get_10101/common/value_data_row.dart';
import 'package:get_10101/trade/new_order_service.dart';
import 'package:get_10101/trade/position_change_notifier.dart';
import 'package:get_10101/trade/position_service.dart';
import 'package:get_10101/trade/quote_change_notifier.dart';
import 'package:get_10101/trade/quote_service.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:get_10101/trade/order_history_table.dart';
import 'package:get_10101/trade/position_table.dart';

class OrderAndPositionTable extends StatefulWidget {
const OrderAndPositionTable({super.key});
Expand All @@ -38,10 +27,10 @@ class OrderAndPositionTableState extends State<OrderAndPositionTable>
isScrollable: false,
tabs: const [
Tab(
text: 'Open',
text: 'Open Position',
),
Tab(
text: 'Pending',
text: 'Order History',
),
],
),
Expand All @@ -50,272 +39,10 @@ class OrderAndPositionTableState extends State<OrderAndPositionTable>
controller: _tabController,
children: const <Widget>[
OpenPositionTable(),
Text("Pending"),
OrderHistoryTable(),
],
))
],
);
}
}

class OpenPositionTable extends StatelessWidget {
const OpenPositionTable({super.key});

@override
Widget build(BuildContext context) {
final positionChangeNotifier = context.watch<PositionChangeNotifier>();
final positions = positionChangeNotifier.getPositions();
final quoteChangeNotifier = context.watch<QuoteChangeNotifier>();
final quote = quoteChangeNotifier.getBestQuote();

if (positions == null) {
return const Center(child: CircularProgressIndicator());
}

if (positions.isEmpty) {
return const Center(child: Text('No data available'));
} else {
return buildTable(positions, quote, context);
}
}

Widget buildTable(List<Position> positions, BestQuote? bestQuote, BuildContext context) {
return Table(
border: TableBorder.symmetric(inside: const BorderSide(width: 2, color: Colors.black)),
defaultVerticalAlignment: TableCellVerticalAlignment.middle,
columnWidths: const {
0: MinColumnWidth(FixedColumnWidth(100.0), FractionColumnWidth(0.1)),
1: MinColumnWidth(FixedColumnWidth(100.0), FractionColumnWidth(0.1)),
2: MinColumnWidth(FixedColumnWidth(100.0), FractionColumnWidth(0.1)),
3: MinColumnWidth(FixedColumnWidth(150.0), FractionColumnWidth(0.1)),
4: MinColumnWidth(FixedColumnWidth(100.0), FractionColumnWidth(0.1)),
5: MinColumnWidth(FixedColumnWidth(100.0), FractionColumnWidth(0.1)),
6: MinColumnWidth(FixedColumnWidth(200.0), FractionColumnWidth(0.25)),
7: FixedColumnWidth(100),
},
children: [
TableRow(
decoration: BoxDecoration(
color: tenTenOnePurple.shade300,
border: Border.all(
width: 1,
),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(10), topRight: Radius.circular(10)),
),
children: [
buildHeaderCell('Quantity'),
buildHeaderCell('Entry Price'),
buildHeaderCell('Liquidation Price'),
buildHeaderCell('Margin'),
buildHeaderCell('Leverage'),
buildHeaderCell('Unrealized PnL'),
buildHeaderCell('Expiry'),
buildHeaderCell('Action'),
],
),
for (var position in positions)
TableRow(
children: [
buildTableCell(Text(position.direction == "Short"
? "-${position.quantity}"
: "+${position.quantity}")),
buildTableCell(Text(position.averageEntryPrice.toString())),
buildTableCell(Text(position.liquidationPrice.toString())),
buildTableCell(Text(position.collateral.toString())),
buildTableCell(Text(position.leverage.formatted())),
buildTableCell(Text(position.pnlSats.toString())),
buildTableCell(
Text("${DateFormat('dd-MM-yyyy – HH:mm').format(position.expiry)} UTC")),
buildTableCell(Center(
child: SizedBox(
width: 100,
child: ElevatedButton(
onPressed: () {
showDialog(
context: context,
builder: (BuildContext context) {
return TradeConfirmationDialog(
direction: Direction.fromString(position.direction),
onConfirmation: () {},
bestQuote: bestQuote,
pnl: position.pnlSats,
fee: position.closingFee,
payout: position.closingFee != null
? Amount(position.collateral.sats +
(position.pnlSats?.sats ?? 0) -
(position.closingFee?.sats ?? 0))
: null,
leverage: position.leverage,
quantity: position.quantity,
);
});
},
child: const Text("Close", style: TextStyle(fontSize: 16))),
),
)),
],
),
],
);
}

TableCell buildHeaderCell(String text) {
return TableCell(
child: Container(
padding: const EdgeInsets.all(10),
alignment: Alignment.center,
child: Text(text,
textAlign: TextAlign.center,
style: const TextStyle(fontWeight: FontWeight.bold, color: Colors.white))));
}

TableCell buildTableCell(Widget child) => TableCell(
child: Center(
child: Container(
padding: const EdgeInsets.all(10), alignment: Alignment.center, child: child)));
}

class TradeConfirmationDialog extends StatelessWidget {
final Direction direction;
final Function() onConfirmation;
final BestQuote? bestQuote;
final Amount? pnl;
final Amount? fee;
final Amount? payout;
final Leverage leverage;
final Usd quantity;

const TradeConfirmationDialog(
{super.key,
required this.direction,
required this.onConfirmation,
required this.bestQuote,
required this.pnl,
required this.fee,
required this.payout,
required this.leverage,
required this.quantity});

@override
Widget build(BuildContext context) {
final messenger = ScaffoldMessenger.of(context);
TenTenOneTheme tradeTheme = Theme.of(context).extension<TenTenOneTheme>()!;

TextStyle dataRowStyle = const TextStyle(fontSize: 14);

Price? price = bestQuote?.bid;
if (direction == Direction.short) {
price = bestQuote?.ask;
}

Color color = direction == Direction.long ? tradeTheme.buy : tradeTheme.sell;

return Dialog(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: SizedBox(
width: 340,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(20),
child: Column(
children: [
const ContractSymbolIcon(),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text("Market ${direction.nameU}",
style:
TextStyle(fontWeight: FontWeight.bold, fontSize: 17, color: color)),
),
Center(
child: Container(
padding: const EdgeInsets.symmetric(vertical: 10),
child: Column(
children: [
Wrap(
runSpacing: 10,
children: [
ValueDataRow(
type: ValueType.fiat,
value: price?.asDouble ?? 0.0,
label: 'Latest Market Price'),
ValueDataRow(
type: ValueType.amount,
value: pnl,
label: 'Unrealized P/L',
valueTextStyle: dataRowStyle.apply(
color: pnl != null
? pnl!.sats.isNegative
? tradeTheme.loss
: tradeTheme.profit
: tradeTheme.disabled)),
ValueDataRow(
type: ValueType.amount,
value: fee,
label: "Fee estimate",
),
ValueDataRow(
type: ValueType.amount,
value: payout,
label: "Payout estimate",
valueTextStyle: TextStyle(
fontSize: dataRowStyle.fontSize,
fontWeight: FontWeight.bold)),
],
),
],
),
),
),
Padding(
padding: const EdgeInsets.only(top: 20.0),
child: RichText(
textAlign: TextAlign.justify,
text: TextSpan(
text:
'By confirming, a closing market order will be created. Once the order is matched your position will be closed.',
style: DefaultTextStyle.of(context).style)),
),
Padding(
padding: const EdgeInsets.only(top: 20.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: () {
Navigator.pop(context);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey, fixedSize: const Size(100, 20)),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () {
NewOrderService.postNewOrder(
leverage, quantity, direction == Direction.long.opposite())
.then((orderId) {
showSnackBar(
messenger, "Closing order created. Order id: $orderId.");
Navigator.pop(context);
});
},
style: ElevatedButton.styleFrom(fixedSize: const Size(100, 20)),
child: const Text('Accept'),
),
],
),
),
],
))
],
),
),
),
);
}
}
Loading
Loading