Skip to content

Commit

Permalink
feat: show open positions
Browse files Browse the repository at this point in the history
Note: it's not responsive yet and doesn't look good on a small screen
Signed-off-by: Philipp Hoenisch <philipp@coblox.tech>
  • Loading branch information
bonomat committed Jan 22, 2024
1 parent b9d4171 commit 634b35e
Show file tree
Hide file tree
Showing 9 changed files with 269 additions and 34 deletions.
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Feat: update api to collaboratively revert a dlc-channel
- Feat: Allow continuing from an offered dlc channel state (offered, settle offered and collab close offered)
- Feat: add a new project `webapp`. Eventually this will have the same functionality as our app (and more) and can be run on a self-hosted server
- Chore: In Webapp API allow requests from any origin (CORS)
- Feat: Allow creating new orders through `webapp`
- Chore (webapp): Add API allow requests from any origin (CORS)
- Feat (webapp): Allow creating new orders through `webapp`
- Feat (webapp): Show open position in trade screen

## [1.7.4] - 2023-12-20

Expand Down
8 changes: 6 additions & 2 deletions mobile/native/src/trade/position/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,15 @@ use rust_decimal::prelude::FromPrimitive;
use rust_decimal::prelude::ToPrimitive;
use rust_decimal::Decimal;
use rust_decimal::RoundingStrategy;
use serde::Serialize;
use time::OffsetDateTime;
use trade::ContractSymbol;
use trade::Direction;

pub mod api;
pub mod handler;

#[derive(Debug, Clone, PartialEq, Copy)]
#[derive(Debug, Clone, PartialEq, Copy, Serialize)]
pub enum PositionState {
/// The position is open
///
Expand Down Expand Up @@ -57,7 +58,7 @@ pub enum PositionState {
Resizing,
}

#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize)]
pub struct Position {
pub leverage: f32,
pub quantity: f32,
Expand All @@ -67,8 +68,11 @@ pub struct Position {
pub liquidation_price: f32,
pub position_state: PositionState,
pub collateral: u64,
#[serde(with = "time::serde::rfc3339")]
pub expiry: OffsetDateTime,
#[serde(with = "time::serde::rfc3339")]
pub updated: OffsetDateTime,
#[serde(with = "time::serde::rfc3339")]
pub created: OffsetDateTime,
pub stable: bool,
}
Expand Down
14 changes: 6 additions & 8 deletions webapp/frontend/lib/common/model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ class Amount implements Formattable {
class Usd implements Formattable {
Decimal _usd = Decimal.zero;

Usd(int usd) {
_usd = Decimal.fromInt(usd);
Usd(double usd) {
_usd = Decimal.parse(usd.toString());
}

int get usd => _usd.toBigInt().toInt();
Expand Down Expand Up @@ -170,15 +170,13 @@ class Price implements Formattable {
}

class Leverage implements Formattable {
int _leverage = 1;

Leverage.one() : _leverage = 1;
double _leverage = 1;

int get toInt => _leverage;
Leverage.one() : _leverage = 1.0;

double get asDouble => _leverage as double;
double get asDouble => _leverage;

Leverage(int leverage) {
Leverage(double leverage) {
_leverage = leverage;
}

Expand Down
153 changes: 153 additions & 0 deletions webapp/frontend/lib/trade/oder_and_position_table.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import 'package:flutter/material.dart';
import 'package:get_10101/common/color.dart';
import 'package:get_10101/logger/logger.dart';
import 'package:get_10101/trade/open_position_service.dart';
import 'package:intl/intl.dart';

class OrderAndPositionTable extends StatefulWidget {
const OrderAndPositionTable({super.key});

@override
OrderAndPositionTableState createState() => OrderAndPositionTableState();
}

class OrderAndPositionTableState extends State<OrderAndPositionTable>
with SingleTickerProviderStateMixin {
late final _tabController = TabController(length: 2, vsync: this);

@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
TabBar(
unselectedLabelColor: Colors.black,
labelColor: tenTenOnePurple,
controller: _tabController,
isScrollable: false,
tabs: const [
Tab(
text: 'Open',
),
Tab(
text: 'Pending',
),
],
),
Container(
constraints: const BoxConstraints(
// Adding constraints to avoid unbounded height, 400 is a random number to avoid pixel overflow
maxHeight: 200,
),
child: TabBarView(
controller: _tabController,
children: const <Widget>[
SimpleTableWidget(),
Text("Pending"),
],
),
),
],
);
}
}

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

@override
Widget build(BuildContext context) {
return FutureBuilder<List<Position>>(
future: OpenPositionsService.fetchOpenPositions(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
logger.i("received ${snapshot.error}");
return const Center(child: Text('Error loading data'));
} else if (!snapshot.hasData || snapshot.data!.isEmpty) {
return const Center(child: Text('No data available'));
} else {
return buildTable(snapshot.data!);
}
},
);
}

Widget buildTable(List<Position> positions) {
return Container(
decoration: BoxDecoration(
border: Border.all(
width: 1,
),
borderRadius: const BorderRadius.all(Radius.circular(10)),
),
child: 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(100.0), FractionColumnWidth(0.1)),
5: MinColumnWidth(FixedColumnWidth(100.0), FractionColumnWidth(0.1)),
6: MinColumnWidth(FixedColumnWidth(100.0), FractionColumnWidth(0.1)),
7: 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('Symbol'),
buildHeaderCell('Quantity'),
buildHeaderCell('Entry Price'),
buildHeaderCell('Liquidation Price'),
buildHeaderCell('Margin'),
buildHeaderCell('Leverage'),
buildHeaderCell('Unrealized PnL'),
buildHeaderCell('Expiry'),
],
),
for (var position in positions)
TableRow(
children: [
buildTableCell(position.contractSymbol),
buildTableCell(position.quantity.formatted()),
buildTableCell(position.averageEntryPrice.formatted()),
buildTableCell(position.liquidationPrice.formatted()),
buildTableCell(position.collateral.formatted()),
buildTableCell(position.leverage.formatted()),
// TODO: we need to get the latest quote to be able to calculate this
buildTableCell("0.0"),
buildTableCell("${DateFormat('dd-MM-yyyy – kk:mm').format(position.expiry)} CET"),
],
),
],
),
);
}

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(String text) => TableCell(
child: Center(
child: Container(
padding: const EdgeInsets.all(10), alignment: Alignment.center, child: Text(text))));
}
63 changes: 63 additions & 0 deletions webapp/frontend/lib/trade/open_position_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import 'dart:convert';
import 'package:flutter/cupertino.dart';
import 'package:get_10101/common/http_client.dart';
import 'package:get_10101/common/model.dart';

class OpenPositionsService {
const OpenPositionsService();

static Future<List<Position>> fetchOpenPositions() async {
final response = await HttpClientManager.instance.get(Uri(path: '/api/positions'));

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

class Position {
final Leverage leverage;
final Usd quantity;
final String contractSymbol;
final String direction;
final Usd averageEntryPrice;
final Usd liquidationPrice;
final String positionState;
final Amount collateral;
final DateTime expiry;
final DateTime updated;
final DateTime created;

Position({
required this.leverage,
required this.quantity,
required this.contractSymbol,
required this.direction,
required this.averageEntryPrice,
required this.liquidationPrice,
required this.positionState,
required this.collateral,
required this.expiry,
required this.updated,
required this.created,
});

factory Position.fromJson(Map<String, dynamic> json) {
return Position(
leverage: Leverage(json['leverage'] as double),
quantity: Usd(json['quantity'] as double),
contractSymbol: json['contract_symbol'] as String,
direction: json['direction'] as String,
averageEntryPrice: Usd(json['average_entry_price'] as double),
liquidationPrice: Usd(json['liquidation_price'] as double),
positionState: json['position_state'] as String,
collateral: Amount(json['collateral']),
expiry: DateTime.parse(json['expiry'] as String),
updated: DateTime.parse(json['updated'] as String),
created: DateTime.parse(json['created'] as String),
);
}
}
45 changes: 25 additions & 20 deletions webapp/frontend/lib/trade/trade_screen.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/oder_and_position_table.dart';
import 'package:get_10101/trade/trade_screen_order_form.dart';

class TradeScreen extends StatefulWidget {
Expand All @@ -21,7 +22,9 @@ class _TradeScreenState extends State<TradeScreen> with SingleTickerProviderStat

@override
Widget build(BuildContext context) {
return NewOrderWidget(tabController: _tabController);
return ListView(
children: [NewOrderWidget(tabController: _tabController), OrderAndPositionTable()],
);
}
}

Expand All @@ -35,13 +38,13 @@ class NewOrderWidget extends StatelessWidget {

@override
Widget build(BuildContext context) {
return SizedBox(
width: 300,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
TabBar(
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
SizedBox(
width: 300,
child: TabBar(
unselectedLabelColor: Colors.black,
labelColor: tenTenOnePurple,
controller: _tabController,
Expand All @@ -54,19 +57,21 @@ class NewOrderWidget extends StatelessWidget {
),
],
),
Expanded(
child: TabBarView(
controller: _tabController,
children: <Widget>[
NewOrderForm(isLong: true),
NewOrderForm(
isLong: false,
)
],
),
),
SizedBox(
height: 400,
width: 300,
child: TabBarView(
controller: _tabController,
children: <Widget>[
NewOrderForm(isLong: true),
NewOrderForm(
isLong: false,
)
],
),
],
),
),
],
);
}
}
3 changes: 1 addition & 2 deletions webapp/frontend/lib/trade/trade_screen_order_form.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import 'package:get_10101/common/amount_text_input_form_field.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/logger/logger.dart';
import 'package:get_10101/trade/new_order_service.dart';

class NewOrderForm extends StatefulWidget {
Expand Down Expand Up @@ -78,7 +77,7 @@ class _NewOrderForm extends State<NewOrderForm> {
label: "Leverage",
textAlign: TextAlign.right,
onChanged: (leverage) => setState(() {
_leverage = Leverage(int.parse(leverage));
_leverage = Leverage(double.parse(leverage));
updateOrderValues();
}),
),
Expand Down
Loading

0 comments on commit 634b35e

Please sign in to comment.