Skip to content

Commit

Permalink
feat(): show order history
Browse files Browse the repository at this point in the history
  • Loading branch information
bonomat committed Jan 31, 2024
1 parent 3bb9fa0 commit d2e1b3b
Show file tree
Hide file tree
Showing 9 changed files with 437 additions and 9 deletions.
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
3 changes: 2 additions & 1 deletion webapp/frontend/lib/trade/order_and_position_table.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:get_10101/common/color.dart';
import 'package:get_10101/trade/order_history_table.dart';
import 'package:get_10101/trade/position_table.dart';

class OrderAndPositionTable extends StatefulWidget {
Expand Down Expand Up @@ -38,7 +39,7 @@ class OrderAndPositionTableState extends State<OrderAndPositionTable>
controller: _tabController,
children: const <Widget>[
OpenPositionTable(),
Text("Pending"),
OrderHistoryTable(),
],
))
],
Expand Down
39 changes: 39 additions & 0 deletions webapp/frontend/lib/trade/order_change_notifier.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:get_10101/logger/logger.dart';
import 'package:get_10101/trade/order_service.dart';
import 'package:get_10101/trade/position_service.dart';

class OrderChangeNotifier extends ChangeNotifier {
final OrderService service;
late Timer timer;

List<Order>? _orders;

OrderChangeNotifier(this.service) {
_refresh();
Timer.periodic(const Duration(seconds: 2), (timer) async {
_refresh();
});
}

void _refresh() async {
try {
final orders = await service.fetchOrders();
_orders = orders;

super.notifyListeners();
} catch (error) {
logger.e(error);
}
}

List<Order>? getOrders() => _orders;

@override
void dispose() {
super.dispose();
timer.cancel();
}
}
128 changes: 128 additions & 0 deletions webapp/frontend/lib/trade/order_history_table.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get_10101/common/color.dart';
import 'package:get_10101/common/direction.dart';
import 'package:get_10101/common/model.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';
import 'package:get_10101/trade/quote_service.dart';
import 'package:get_10101/trade/trade_confirmation_dialog.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';

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

@override
Widget build(BuildContext context) {
final orderChangeNotified = context.watch<OrderChangeNotifier>();
final orders = orderChangeNotified.getOrders();

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

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

Widget buildTable(List<Order> orders, BuildContext context) {
orders.sort((a, b) => b.creationTimestamp.compareTo(a.creationTimestamp));

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(100.0), FractionColumnWidth(0.1)),
4: MinColumnWidth(FixedColumnWidth(200.0), FractionColumnWidth(0.2)),
},
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('State'),
buildHeaderCell('Price'),
buildHeaderCell('Quantity'),
buildHeaderCell('Leverage'),
buildHeaderCell('Timestamp'),
],
),
for (var order in orders)
TableRow(
children: [
buildTableCell(Tooltip(message: order.state.asString, child: stateToIcon(order))),
// buildTableCell(Text(order.id)),
buildTableCell(Text(order.price != null ? order.price.toString() : "NaN")),
buildTableCell(
Text(order.direction == "Short" ? "-${order.quantity}" : "+${order.quantity}")),
buildTableCell(Text("${order.leverage.formatted()}x")),
buildTableCell(
Text("${DateFormat('dd-MM-yyyy – HH:mm').format(order.creationTimestamp)} UTC")),
],
),
],
);
}

Widget stateToIcon(Order order) {
const double size = 16.0;
var icon = switch (order.state) {
OrderState.initial =>
const SizedBox(width: size, height: size, child: CircularProgressIndicator()),
OrderState.rejected => const Icon(
FontAwesomeIcons.circleExclamation,
size: size,
),
OrderState.open =>
const SizedBox(width: size, height: size, child: CircularProgressIndicator()),
OrderState.filling =>
const SizedBox(width: size, height: size, child: CircularProgressIndicator()),
OrderState.failed => const Icon(
FontAwesomeIcons.circleExclamation,
color: Colors.red,
size: size,
),
OrderState.filled => const Icon(
FontAwesomeIcons.check,
size: size,
),
OrderState.unknown => const Icon(
FontAwesomeIcons.circleExclamation,
size: size,
)
};
return icon;
}

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)));
}
112 changes: 112 additions & 0 deletions webapp/frontend/lib/trade/order_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import 'dart:convert';
import 'package:flutter/cupertino.dart';
import 'package:get_10101/common/contract_symbol.dart';
import 'package:get_10101/common/direction.dart';
import 'package:get_10101/common/http_client.dart';
import 'package:get_10101/common/model.dart';
import 'package:get_10101/common/order_type.dart';

class OrderService {
const OrderService();

Future<List<Order>> fetchOrders() async {
final response = await HttpClientManager.instance.get(Uri(path: '/api/orders'));

if (response.statusCode == 200) {
final List<dynamic> jsonData = jsonDecode(response.body);
return jsonData.map((orderData) => Order.fromJson(orderData)).toList();
} else {
throw FlutterError("Could not fetch orders");
}
}
}

class Order {
final String id; //: Uuid,
final Leverage leverage; //: f32,
final Usd quantity; //: f32,
final Usd? price; //: f32,
final ContractSymbol contractSymbol; //: ContractSymbol,
final Direction direction; //: Direction,
final OrderType orderType; //: OrderType,
// TODO: define a state
final OrderState state; //: OrderState,
final DateTime creationTimestamp; //: OffsetDateTime,
// TODO: define failure reason
final String? failureReason; //: Option<FailureReason>,

Order(
{required this.id,
required this.leverage,
required this.quantity,
required this.price,
required this.contractSymbol,
required this.direction,
required this.orderType,
required this.state,
required this.creationTimestamp,
required this.failureReason});

factory Order.fromJson(Map<String, dynamic> json) {
return Order(
id: json['id'] as String,
leverage: Leverage(json['leverage'] as double),
quantity: Usd(json['quantity'] as double),
price: json['price'] != null ? Usd(json['price'] as double) : null,
contractSymbol: ContractSymbol.btcusd,
direction: Direction.fromString(json['direction']),
creationTimestamp: DateTime.parse(json['creation_timestamp'] as String),
orderType: OrderType.fromString(json['order_type'] as String),
state: OrderState.fromString(json['state'] as String), //json['state'] as String,
failureReason: json['failure_reason'], // json['failure_reason'],
);
}
}

enum OrderState {
initial,
rejected,
open,
filling,
failed,
filled,
unknown;

String get asString {
switch (this) {
case OrderState.initial:
return "Initial";
case OrderState.rejected:
return "Rejected";
case OrderState.open:
return "Open";
case OrderState.filling:
return "Filling";
case OrderState.failed:
return "Failed";
case OrderState.filled:
return "Filled";
case OrderState.unknown:
return "Unknown";
}
}

static OrderState fromString(String value) {
switch (value.toLowerCase()) {
case 'initial':
return OrderState.initial;
case 'rejected':
return OrderState.rejected;
case 'open':
return OrderState.open;
case 'filling':
return OrderState.filling;
case 'filled':
return OrderState.filled;
case 'failed':
return OrderState.failed;
default:
throw ArgumentError('Invalid OrderState: $json');
}
}
}
Loading

0 comments on commit d2e1b3b

Please sign in to comment.