diff --git a/CHANGELOG.md b/CHANGELOG.md index b337e3e01..4790ee50b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - Find match on an expired position +- Show loading screen when app starts with an expired position ## [1.2.6] - 2023-09-06 diff --git a/coordinator/migrations/2023-09-08-090155_add_order_reason/down.sql b/coordinator/migrations/2023-09-08-090155_add_order_reason/down.sql new file mode 100644 index 000000000..1b6cf6e72 --- /dev/null +++ b/coordinator/migrations/2023-09-08-090155_add_order_reason/down.sql @@ -0,0 +1,5 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE orders + DROP COLUMN "order_reason"; + +DROP TYPE "OrderReason_Type"; \ No newline at end of file diff --git a/coordinator/migrations/2023-09-08-090155_add_order_reason/up.sql b/coordinator/migrations/2023-09-08-090155_add_order_reason/up.sql new file mode 100644 index 000000000..29521a6d0 --- /dev/null +++ b/coordinator/migrations/2023-09-08-090155_add_order_reason/up.sql @@ -0,0 +1,8 @@ +-- Your SQL goes here +CREATE TYPE "OrderReason_Type" AS ENUM ( + 'Manual', + 'Expired' +); + +ALTER TABLE "orders" + ADD COLUMN "order_reason" "OrderReason_Type" NOT NULL DEFAULT 'Manual'; \ No newline at end of file diff --git a/coordinator/src/node/expired_positions.rs b/coordinator/src/node/expired_positions.rs index af979e99b..f0cc02ae2 100644 --- a/coordinator/src/node/expired_positions.rs +++ b/coordinator/src/node/expired_positions.rs @@ -13,6 +13,7 @@ use orderbook_commons::Match; use orderbook_commons::MatchState; use orderbook_commons::NewOrder; use orderbook_commons::Order; +use orderbook_commons::OrderReason; use orderbook_commons::OrderState; use orderbook_commons::OrderType; use rust_decimal::prelude::ToPrimitive; @@ -95,6 +96,7 @@ pub async fn close(node: Node, trading_sender: mpsc::Sender) -> let (sender, mut receiver) = mpsc::channel::>(1); let message = TradingMessage::NewOrder(NewOrderMessage { new_order: new_order.clone(), + order_reason: OrderReason::Expired, sender, }); diff --git a/coordinator/src/orderbook/db/custom_types.rs b/coordinator/src/orderbook/db/custom_types.rs index 4307d25fb..fa4d20d47 100644 --- a/coordinator/src/orderbook/db/custom_types.rs +++ b/coordinator/src/orderbook/db/custom_types.rs @@ -1,5 +1,6 @@ use crate::schema::sql_types::DirectionType; use crate::schema::sql_types::MatchStateType; +use crate::schema::sql_types::OrderReasonType; use crate::schema::sql_types::OrderStateType; use crate::schema::sql_types::OrderTypeType; use diesel::deserialize; @@ -134,6 +135,44 @@ impl FromSql for OrderState { } } +#[derive(Debug, Clone, Copy, PartialEq, FromSqlRow, AsExpression)] +#[diesel(sql_type = OrderReasonType)] +pub(crate) enum OrderReason { + /// The order has been created manually by the user. + Manual, + /// The order has been create automatically as the position expired. + Expired, +} + +impl QueryId for OrderReasonType { + type QueryId = OrderReasonType; + const HAS_STATIC_QUERY_ID: bool = false; + + fn query_id() -> Option { + None + } +} + +impl ToSql for OrderReason { + fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Pg>) -> serialize::Result { + match *self { + OrderReason::Manual => out.write_all(b"Manual")?, + OrderReason::Expired => out.write_all(b"Expired")?, + } + Ok(IsNull::No) + } +} + +impl FromSql for OrderReason { + fn from_sql(bytes: PgValue<'_>) -> deserialize::Result { + match bytes.as_bytes() { + b"Manual" => Ok(OrderReason::Manual), + b"Expired" => Ok(OrderReason::Expired), + _ => Err("Unrecognized enum variant".into()), + } + } +} + #[derive(Debug, Clone, Copy, PartialEq, FromSqlRow, AsExpression)] #[diesel(sql_type = MatchStateType)] pub(crate) enum MatchState { diff --git a/coordinator/src/orderbook/db/orders.rs b/coordinator/src/orderbook/db/orders.rs index d8643a947..4b46d730c 100644 --- a/coordinator/src/orderbook/db/orders.rs +++ b/coordinator/src/orderbook/db/orders.rs @@ -1,5 +1,6 @@ use crate::db::positions::ContractSymbol; use crate::orderbook::db::custom_types::Direction; +use crate::orderbook::db::custom_types::OrderReason; use crate::orderbook::db::custom_types::OrderState; use crate::orderbook::db::custom_types::OrderType; use crate::schema::orders; @@ -9,6 +10,7 @@ use diesel::result::QueryResult; use diesel::PgConnection; use orderbook_commons::NewOrder as OrderbookNewOrder; use orderbook_commons::Order as OrderbookOrder; +use orderbook_commons::OrderReason as OrderBookOrderReason; use orderbook_commons::OrderState as OrderBookOrderState; use orderbook_commons::OrderType as OrderBookOrderType; use rust_decimal::prelude::FromPrimitive; @@ -93,6 +95,7 @@ struct Order { pub order_state: OrderState, pub contract_symbol: ContractSymbol, pub leverage: f32, + pub order_reason: OrderReason, } impl From for OrderbookOrder { @@ -111,6 +114,25 @@ impl From for OrderbookOrder { timestamp: value.timestamp, expiry: value.expiry, order_state: value.order_state.into(), + order_reason: value.order_reason.into(), + } + } +} + +impl From for OrderBookOrderReason { + fn from(value: OrderReason) -> Self { + match value { + OrderReason::Manual => OrderBookOrderReason::Manual, + OrderReason::Expired => OrderBookOrderReason::Expired, + } + } +} + +impl From for OrderReason { + fn from(value: OrderBookOrderReason) -> Self { + match value { + OrderBookOrderReason::Manual => OrderReason::Manual, + OrderBookOrderReason::Expired => OrderReason::Expired, } } } @@ -126,6 +148,7 @@ struct NewOrder { pub quantity: f32, pub order_type: OrderType, pub expiry: OffsetDateTime, + pub order_reason: OrderReason, pub contract_symbol: ContractSymbol, pub leverage: f32, } @@ -149,6 +172,7 @@ impl From for NewOrder { .expect("To be able to convert decimal to f32"), order_type: value.order_type.into(), expiry: value.expiry, + order_reason: OrderReason::Manual, contract_symbol: value.contract_symbol.into(), leverage: value.leverage, } @@ -211,9 +235,17 @@ pub fn get_all_orders( } /// Returns the number of affected rows: 1. -pub fn insert(conn: &mut PgConnection, order: OrderbookNewOrder) -> QueryResult { +pub fn insert( + conn: &mut PgConnection, + order: OrderbookNewOrder, + order_reason: OrderBookOrderReason, +) -> QueryResult { + let new_order = NewOrder { + order_reason: OrderReason::from(order_reason), + ..NewOrder::from(order) + }; let order: Order = diesel::insert_into(orders::table) - .values(NewOrder::from(order)) + .values(new_order) .get_result(conn)?; Ok(OrderbookOrder::from(order)) diff --git a/coordinator/src/orderbook/routes.rs b/coordinator/src/orderbook/routes.rs index 3dac1014d..3e90247a6 100644 --- a/coordinator/src/orderbook/routes.rs +++ b/coordinator/src/orderbook/routes.rs @@ -18,6 +18,7 @@ use diesel::r2d2::PooledConnection; use diesel::PgConnection; use orderbook_commons::NewOrder; use orderbook_commons::Order; +use orderbook_commons::OrderReason; use orderbook_commons::OrderbookMsg; use serde::de; use serde::Deserialize; @@ -93,7 +94,11 @@ pub async fn post_order( ) -> Result, AppError> { let (sender, mut receiver) = mpsc::channel::>(1); - let message = TradingMessage::NewOrder(NewOrderMessage { new_order, sender }); + let message = TradingMessage::NewOrder(NewOrderMessage { + new_order, + order_reason: OrderReason::Manual, + sender, + }); state.trading_sender.send(message).await.map_err(|e| { AppError::InternalServerError(format!("Failed to send new order message: {e:#}")) })?; diff --git a/coordinator/src/orderbook/tests/sample_test.rs b/coordinator/src/orderbook/tests/sample_test.rs index 964161485..d92e93c95 100644 --- a/coordinator/src/orderbook/tests/sample_test.rs +++ b/coordinator/src/orderbook/tests/sample_test.rs @@ -4,6 +4,7 @@ use crate::orderbook::tests::setup_db; use crate::orderbook::tests::start_postgres; use bitcoin::secp256k1::PublicKey; use orderbook_commons::NewOrder; +use orderbook_commons::OrderReason; use orderbook_commons::OrderType; use rust_decimal_macros::dec; use std::str::FromStr; @@ -28,6 +29,7 @@ async fn crud_test() { let order = orders::insert( &mut conn, dummy_order(OffsetDateTime::now_utc() + Duration::minutes(1)), + OrderReason::Manual, ) .unwrap(); @@ -56,11 +58,13 @@ async fn test_filter_expired_orders() { let order = orders::insert( &mut conn, dummy_order(OffsetDateTime::now_utc() + Duration::minutes(1)), + OrderReason::Manual, ) .unwrap(); let _ = orders::insert( &mut conn, dummy_order(OffsetDateTime::now_utc() - Duration::minutes(1)), + OrderReason::Manual, ) .unwrap(); diff --git a/coordinator/src/orderbook/trading.rs b/coordinator/src/orderbook/trading.rs index 9528a27fc..dfaa5797b 100644 --- a/coordinator/src/orderbook/trading.rs +++ b/coordinator/src/orderbook/trading.rs @@ -14,6 +14,7 @@ use orderbook_commons::FilledWith; use orderbook_commons::Match; use orderbook_commons::NewOrder; use orderbook_commons::Order; +use orderbook_commons::OrderReason; use orderbook_commons::OrderState; use orderbook_commons::OrderType; use orderbook_commons::OrderbookMsg; @@ -44,6 +45,7 @@ pub enum TradingMessage { pub struct NewOrderMessage { pub new_order: NewOrder, + pub order_reason: OrderReason, pub sender: mpsc::Sender>, } @@ -119,12 +121,22 @@ impl Trading { TradingMessage::NewOrder(new_order_msg) => { // todo(holzeis): spawn a task here to not block other users from trading if // this is taking some time. - let result = self.process_new_order(new_order_msg.new_order).await; + let new_order = new_order_msg.new_order; + let result = self + .process_new_order(new_order, new_order_msg.order_reason) + .await; new_order_msg.sender.send(result).await?; } TradingMessage::NewUser(new_user_msg) => { + tracing::info!(trader_id=%new_user_msg.new_user, "User logged in to 10101"); + self.authenticated_users .insert(new_user_msg.new_user, new_user_msg.sender); + + // todo(holzeis): spawn a task here to not block other users from trading if + // this is taking some time. + tracing::debug!(trader_id=%new_user_msg.new_user, "Checking if the user needs to be notified about pending matches"); + self.process_pending_match(new_user_msg.new_user).await?; } } } @@ -137,7 +149,11 @@ impl Trading { /// /// Limit order: update price feed /// Market order: find match and notify traders - async fn process_new_order(&self, new_order: NewOrder) -> Result { + async fn process_new_order( + &self, + new_order: NewOrder, + order_reason: OrderReason, + ) -> Result { tracing::info!(trader_id=%new_order.trader_id, "Received a new {:?} order", new_order.order_type); if new_order.order_type == OrderType::Limit && new_order.price == Decimal::ZERO { @@ -147,7 +163,7 @@ impl Trading { } let mut conn = self.pool.get()?; - let order = orders::insert(&mut conn, new_order.clone()) + let order = orders::insert(&mut conn, new_order.clone(), order_reason) .map_err(|e| anyhow!("Failed to insert new order into db: {e:#}"))?; if new_order.order_type == OrderType::Limit { @@ -156,6 +172,17 @@ impl Trading { .send(OrderbookMsg::NewOrder(order.clone())) .map_err(|error| anyhow!("Could not update price feed due to '{error}'"))?; } else { + // reject new order if there is already a matched order waiting for execution. + if let Some(order) = orders::get_by_trader_id_and_state( + &mut conn, + new_order.trader_id, + OrderState::Match, + )? { + bail!(TradingError::InvalidOrder(format!( + "trader_id={}, order_id={}, Order is currently in execution. Can't accept new orders until the order execution is finished" + , new_order.trader_id, order.id))); + } + let opposite_direction_orders = orders::all_by_direction_and_type( &mut conn, order.direction.opposite(), @@ -191,18 +218,27 @@ impl Trading { for match_param in match_params { matches::insert(&mut conn, match_param)?; - let trader_id = match_param.trader_id.to_string(); + let trader_id = match_param.trader_id; let order_id = match_param.filled_with.order_id.to_string(); - tracing::info!(trader_id, order_id, "Notifying trader about match"); + tracing::info!(%trader_id, order_id, "Notifying trader about match"); - let order_state = match notify_trader(match_param, &self.authenticated_users).await + let message = match &order.order_reason { + OrderReason::Manual => OrderbookMsg::Match(match_param.filled_with.clone()), + OrderReason::Expired => OrderbookMsg::AsyncMatch { + order: order.clone(), + filled_with: match_param.filled_with.clone(), + }, + }; + + let order_state = match notify_trader(trader_id, message, &self.authenticated_users) + .await { Ok(()) => { - tracing::debug!(trader_id, order_id, "Successfully notified trader"); + tracing::debug!(%trader_id, order_id, "Successfully notified trader"); OrderState::Match } Err(e) => { - tracing::warn!(trader_id, order_id, "{e:#}"); + tracing::warn!(%trader_id, order_id, "{e:#}"); // todo(holzeis): send push notification to user if order.order_type == OrderType::Limit { @@ -220,7 +256,7 @@ impl Trading { }; tracing::debug!( - trader_id, + %trader_id, order_id, "Updating the order state to {order_state:?}" ); @@ -230,6 +266,30 @@ impl Trading { Ok(order) } + + /// Notifies the trader if a pending match is waiting for them. + pub async fn process_pending_match(&self, trader_id: PublicKey) -> Result<()> { + let mut conn = self.pool.get()?; + if let Some(order) = + orders::get_by_trader_id_and_state(&mut conn, trader_id, OrderState::Match)? + { + tracing::debug!(%trader_id, order_id=%order.id, "Notifying trader about pending match"); + + let matches = matches::get_matches_by_order_id(&mut conn, order.id)?; + let filled_with = FilledWith::try_from(matches)?; + + let message = match order.order_reason { + OrderReason::Manual => OrderbookMsg::Match(filled_with), + OrderReason::Expired => OrderbookMsg::AsyncMatch { order, filled_with }, + }; + + if let Err(e) = notify_trader(trader_id, message, &self.authenticated_users).await { + tracing::warn!("Failed to notify trader. Error: {e:#}"); + } + } + + Ok(()) + } } /// Matches a provided market order with limit orders from the DB @@ -365,13 +425,14 @@ fn sort_orders(mut orders: Vec, is_long: bool) -> Vec { } async fn notify_trader( - match_params: &TraderMatchParams, + trader_id: PublicKey, + message: OrderbookMsg, traders: &HashMap>, ) -> Result<()> { - match traders.get(&match_params.trader_id) { + match traders.get(&trader_id) { None => bail!("Trader is not connected"), Some(sender) => sender - .send(OrderbookMsg::Match(match_params.filled_with.clone())) + .send(message) .await .map_err(|err| anyhow!("Connection lost to trader {err:#}")), } @@ -391,6 +452,7 @@ pub mod tests { use orderbook_commons::FilledWith; use orderbook_commons::Match; use orderbook_commons::Order; + use orderbook_commons::OrderReason; use orderbook_commons::OrderState; use orderbook_commons::OrderType; use orderbook_commons::OrderbookMsg; @@ -427,6 +489,7 @@ pub mod tests { timestamp: OffsetDateTime::now_utc() + timestamp_delay, expiry: OffsetDateTime::now_utc() + Duration::minutes(1), order_state: OrderState::Open, + order_reason: OrderReason::Manual, } } @@ -567,6 +630,7 @@ pub mod tests { timestamp: OffsetDateTime::now_utc(), expiry: OffsetDateTime::now_utc() + Duration::minutes(1), order_state: OrderState::Open, + order_reason: OrderReason::Manual, }; let matched_orders = match_order(&order, all_orders).unwrap().unwrap(); @@ -641,6 +705,7 @@ pub mod tests { timestamp: OffsetDateTime::now_utc(), expiry: OffsetDateTime::now_utc() + Duration::minutes(1), order_state: OrderState::Open, + order_reason: OrderReason::Manual, }; assert!(match_order(&order, all_orders).is_err()); @@ -691,6 +756,7 @@ pub mod tests { timestamp: OffsetDateTime::now_utc(), expiry: OffsetDateTime::now_utc() + Duration::minutes(1), order_state: OrderState::Open, + order_reason: OrderReason::Manual, }; let matched_orders = match_order(&order, all_orders).unwrap(); @@ -751,7 +817,13 @@ pub mod tests { traders.insert(trader_pub_key, trader_sender); for match_param in matched_orders.matches() { - notify_trader(match_param, &traders).await.unwrap(); + notify_trader( + match_param.trader_id, + OrderbookMsg::Match(match_param.filled_with.clone()), + &traders, + ) + .await + .unwrap(); } let maker_msg = maker_receiver.recv().await.unwrap(); diff --git a/coordinator/src/schema.rs b/coordinator/src/schema.rs index ae0f17cf0..b63f97f58 100644 --- a/coordinator/src/schema.rs +++ b/coordinator/src/schema.rs @@ -21,6 +21,10 @@ pub mod sql_types { #[diesel(postgres_type(name = "MatchState_Type"))] pub struct MatchStateType; + #[derive(diesel::sql_types::SqlType)] + #[diesel(postgres_type(name = "OrderReason_Type"))] + pub struct OrderReasonType; + #[derive(diesel::sql_types::SqlType)] #[diesel(postgres_type(name = "OrderState_Type"))] pub struct OrderStateType; @@ -97,6 +101,7 @@ diesel::table! { order_state -> OrderStateType, contract_symbol -> ContractSymbolType, leverage -> Float4, + order_reason -> OrderReasonType, } } diff --git a/crates/orderbook-commons/src/lib.rs b/crates/orderbook-commons/src/lib.rs index 7f61e401a..3dd77b8c7 100644 --- a/crates/orderbook-commons/src/lib.rs +++ b/crates/orderbook-commons/src/lib.rs @@ -1,3 +1,7 @@ +pub use crate::order_matching_fee::order_matching_fee_taker; +pub use crate::price::best_current_price; +pub use crate::price::Price; +pub use crate::price::Prices; use anyhow::ensure; use rust_decimal::prelude::ToPrimitive; use rust_decimal::Decimal; @@ -9,6 +13,8 @@ use serde::Serialize; use sha2::digest::FixedOutput; use sha2::Digest; use sha2::Sha256; +use std::str::FromStr; +use time::Duration; use time::OffsetDateTime; use trade::ContractSymbol; use trade::Direction; @@ -17,11 +23,6 @@ use uuid::Uuid; mod order_matching_fee; mod price; -pub use crate::order_matching_fee::order_matching_fee_taker; -pub use crate::price::best_current_price; -pub use crate::price::Price; -pub use crate::price::Prices; - /// The prefix used in the description field of an order-matching fee invoice to be paid by a taker. pub const FEE_INVOICE_DESCRIPTION_PREFIX_TAKER: &str = "taker-fee-"; @@ -33,6 +34,12 @@ pub enum OrderState { Failed, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub enum OrderReason { + Manual, + Expired, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct Order { pub id: Uuid, @@ -51,6 +58,7 @@ pub struct Order { #[serde(with = "time::serde::rfc3339")] pub expiry: OffsetDateTime, pub order_state: OrderState, + pub order_reason: OrderReason, } #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] @@ -117,6 +125,10 @@ pub enum OrderbookMsg { InvalidAuthentication(String), Authenticated, Match(FilledWith), + AsyncMatch { + order: Order, + filled_with: FilledWith, + }, } /// A match for an order @@ -306,6 +318,42 @@ pub struct Matches { pub updated_at: OffsetDateTime, } +impl TryFrom> for FilledWith { + type Error = anyhow::Error; + + fn try_from(matches: Vec) -> Result { + ensure!( + !matches.is_empty(), + "Need at least one matches record to construct a FilledWith" + ); + + let order_id = matches.first().expect("to have at least on match").order_id; + let oracle_pk = XOnlyPublicKey::from_str( + "16f88cf7d21e6c0f46bcbc983a4e3b19726c6c98858cc31c83551a88fde171c0", + ) + .expect("To be a valid pubkey"); + + let tomorrow = OffsetDateTime::now_utc().date() + Duration::days(7); + let expiry_timestamp = tomorrow.midnight().assume_utc(); + + Ok(Self { + order_id, + expiry_timestamp, + oracle_pk, + matches: matches + .iter() + .map(|m| Match { + id: m.id, + order_id: m.order_id, + quantity: m.quantity, + pubkey: m.match_trader_id, + execution_price: m.execution_price, + }) + .collect(), + }) + } +} + #[cfg(test)] mod test { use crate::FilledWith; diff --git a/crates/orderbook-commons/src/price.rs b/crates/orderbook-commons/src/price.rs index ee27d973e..aa9f266de 100644 --- a/crates/orderbook-commons/src/price.rs +++ b/crates/orderbook-commons/src/price.rs @@ -71,6 +71,7 @@ mod test { use crate::price::best_ask_price; use crate::price::best_bid_price; use crate::Order; + use crate::OrderReason; use crate::OrderState; use crate::OrderType; use rust_decimal::Decimal; @@ -106,6 +107,7 @@ mod test { timestamp: OffsetDateTime::now_utc(), expiry: OffsetDateTime::now_utc(), order_state, + order_reason: OrderReason::Manual, } } diff --git a/crates/tests-e2e/src/test_subscriber.rs b/crates/tests-e2e/src/test_subscriber.rs index 7d429e101..9188da433 100644 --- a/crates/tests-e2e/src/test_subscriber.rs +++ b/crates/tests-e2e/src/test_subscriber.rs @@ -203,6 +203,9 @@ impl Senders { native::event::EventInternal::PaymentClaimed(_amount_msats) => { unreachable!("PaymentClaimed event should not be sent to the subscriber"); } + native::event::EventInternal::AsyncTrade(_order_id) => { + // ignored + } } Ok(()) } diff --git a/mobile/lib/features/trade/application/order_service.dart b/mobile/lib/features/trade/application/order_service.dart index df927eaac..6c9bb1a7f 100644 --- a/mobile/lib/features/trade/application/order_service.dart +++ b/mobile/lib/features/trade/application/order_service.dart @@ -28,4 +28,14 @@ class OrderService { return orders; } + + Future fetchAsyncOrder() async { + rust.Order? order = await rust.api.getAsyncOrder(); + + if (order == null) { + return null; + } + + return Order.fromApi(order); + } } diff --git a/mobile/lib/features/trade/async_order_change_notifier.dart b/mobile/lib/features/trade/async_order_change_notifier.dart new file mode 100644 index 000000000..0b9b5f91b --- /dev/null +++ b/mobile/lib/features/trade/async_order_change_notifier.dart @@ -0,0 +1,73 @@ +import 'package:f_logs/model/flog/flog.dart'; +import 'package:flutter/material.dart'; +import 'package:get_10101/bridge_generated/bridge_definitions.dart' as bridge; +import 'package:get_10101/common/application/event_service.dart'; +import 'package:get_10101/common/global_keys.dart'; +import 'package:get_10101/features/trade/application/order_service.dart'; +import 'package:get_10101/features/trade/domain/order.dart'; +import 'package:get_10101/features/trade/order_submission_status_dialog.dart'; +import 'package:provider/provider.dart'; + +class AsyncOrderChangeNotifier extends ChangeNotifier implements Subscriber { + late OrderService _orderService; + Order? forceOrder; + + Future initialize() async { + Order? order = await _orderService.fetchAsyncOrder(); + + if (order != null) { + notifyListeners(); + } + } + + AsyncOrderChangeNotifier(OrderService orderService) { + _orderService = orderService; + } + + @override + void notify(bridge.Event event) { + if (event is bridge.Event_AsyncTrade) { + OrderReason reason = OrderReason.fromApi(event.field0); + FLog.debug(text: "Received a force trade event. Reason: $reason"); + WidgetsBinding.instance.addPostFrameCallback((_) async { + showDialog( + context: shellNavigatorKey.currentContext!, + builder: (context) { + Order? forceOrder = context.watch().forceOrder; + + OrderSubmissionStatusDialogType type = OrderSubmissionStatusDialogType.pendingSubmit; + switch (forceOrder?.state) { + case OrderState.open: + type = OrderSubmissionStatusDialogType.successfulSubmit; + case OrderState.failed: + type = OrderSubmissionStatusDialogType.failedFill; + case OrderState.filled: + type = OrderSubmissionStatusDialogType.filled; + case null: + type = OrderSubmissionStatusDialogType.pendingSubmit; + } + + late Widget content; + switch (reason) { + case OrderReason.expired: + content = const Text("Your position has been closed due to expiry."); + case OrderReason.manual: + FLog.error(text: "A manual order should not appear as an async trade!"); + content = Container(); + } + + return OrderSubmissionStatusDialog(title: "Catching up!", type: type, content: content); + }, + ); + }); + } else if (event is bridge.Event_OrderUpdateNotification) { + Order order = Order.fromApi(event.field0); + if (order.reason != OrderReason.manual) { + forceOrder = order; + notifyListeners(); + } + } else { + FLog.warning(text: "Received unexpected event: ${event.toString()}"); + } + } +} diff --git a/mobile/lib/features/trade/domain/order.dart b/mobile/lib/features/trade/domain/order.dart index 118f3af24..60a43f205 100644 --- a/mobile/lib/features/trade/domain/order.dart +++ b/mobile/lib/features/trade/domain/order.dart @@ -1,7 +1,25 @@ +import 'package:get_10101/bridge_generated/bridge_definitions.dart' as bridge; import 'package:get_10101/features/trade/domain/contract_symbol.dart'; import 'package:get_10101/features/trade/domain/direction.dart'; import 'package:get_10101/features/trade/domain/leverage.dart'; -import 'package:get_10101/bridge_generated/bridge_definitions.dart' as bridge; + +enum OrderReason { + manual, + expired; + + static OrderReason fromApi(bridge.OrderReason orderReason) { + switch (orderReason) { + case bridge.OrderReason.Manual: + return OrderReason.manual; + case bridge.OrderReason.Expired: + return OrderReason.expired; + } + } + + static bridge.OrderReason apiDummy() { + return bridge.OrderReason.Manual; + } +} enum OrderState { open, @@ -42,6 +60,7 @@ class Order { final OrderType type; final double? executionPrice; final DateTime creationTimestamp; + final OrderReason reason; Order( {required this.id, @@ -52,7 +71,8 @@ class Order { required this.state, required this.type, required this.creationTimestamp, - this.executionPrice}); + this.executionPrice, + required this.reason}); static Order fromApi(bridge.Order order) { return Order( @@ -64,7 +84,8 @@ class Order { state: OrderState.fromApi(order.state), type: OrderType.fromApi(order.orderType), executionPrice: order.executionPrice, - creationTimestamp: DateTime.fromMillisecondsSinceEpoch(order.creationTimestamp * 1000)); + creationTimestamp: DateTime.fromMillisecondsSinceEpoch(order.creationTimestamp * 1000), + reason: OrderReason.fromApi(order.reason)); } static bridge.Order apiDummy() { @@ -77,6 +98,7 @@ class Order { orderType: bridge.OrderType.market(), state: bridge.OrderState.Open, creationTimestamp: 0, - orderExpiryTimestamp: 0); + orderExpiryTimestamp: 0, + reason: bridge.OrderReason.Manual); } } diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 8bdc1981b..e5e2391ce 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -24,6 +24,7 @@ import 'package:get_10101/features/trade/application/candlestick_service.dart'; import 'package:get_10101/features/trade/application/order_service.dart'; import 'package:get_10101/features/trade/application/position_service.dart'; import 'package:get_10101/features/trade/application/trade_values_service.dart'; +import 'package:get_10101/features/trade/async_order_change_notifier.dart'; import 'package:get_10101/features/trade/candlestick_change_notifier.dart'; import 'package:get_10101/features/trade/domain/order.dart'; import 'package:get_10101/features/trade/domain/position.dart'; @@ -87,6 +88,7 @@ void main() { create: (context) => CandlestickChangeNotifier(const CandlestickService())), ChangeNotifierProvider(create: (context) => ServiceStatusNotifier()), ChangeNotifierProvider(create: (context) => ChannelStatusNotifier()), + ChangeNotifierProvider(create: (context) => AsyncOrderChangeNotifier(OrderService())), Provider(create: (context) => Environment.parse()), Provider(create: (context) => channelInfoService) ], child: const TenTenOneApp())); @@ -459,6 +461,7 @@ void subscribeToNotifiers(BuildContext context) { final serviceStatusNotifier = context.read(); final channelStatusNotifier = context.read(); final stableValuesChangeNotifier = context.read(); + final asyncOrderChangeNotifier = context.read(); eventService.subscribe( orderChangeNotifier, bridge.Event.orderUpdateNotification(Order.apiDummy())); @@ -489,6 +492,10 @@ void subscribeToNotifiers(BuildContext context) { eventService.subscribe( serviceStatusNotifier, bridge.Event.serviceHealthUpdate(serviceUpdateApiDummy())); + eventService.subscribe( + asyncOrderChangeNotifier, bridge.Event.orderUpdateNotification(Order.apiDummy())); + eventService.subscribe(asyncOrderChangeNotifier, bridge.Event.asyncTrade(OrderReason.apiDummy())); + channelStatusNotifier.subscribe(eventService); eventService.subscribe( diff --git a/mobile/native/migrations/2023-09-06-130413_add_order_reason/down.sql b/mobile/native/migrations/2023-09-06-130413_add_order_reason/down.sql new file mode 100644 index 000000000..3be54f01c --- /dev/null +++ b/mobile/native/migrations/2023-09-06-130413_add_order_reason/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE + orders DROP COLUMN "reason"; diff --git a/mobile/native/migrations/2023-09-06-130413_add_order_reason/up.sql b/mobile/native/migrations/2023-09-06-130413_add_order_reason/up.sql new file mode 100644 index 000000000..4c1087a94 --- /dev/null +++ b/mobile/native/migrations/2023-09-06-130413_add_order_reason/up.sql @@ -0,0 +1,5 @@ +-- Your SQL goes here +ALTER TABLE + orders + ADD + COLUMN "reason" TEXT NOT NULL DEFAULT 'Manual'; diff --git a/mobile/native/src/api.rs b/mobile/native/src/api.rs index 781cf50c0..234a305a9 100644 --- a/mobile/native/src/api.rs +++ b/mobile/native/src/api.rs @@ -202,6 +202,13 @@ pub async fn get_orders() -> Result> { Ok(orders) } +#[tokio::main(flavor = "current_thread")] +pub async fn get_async_order() -> Result> { + let order = order::handler::get_async_order()?; + let order = order.map(|order| order.into()); + Ok(order) +} + #[tokio::main(flavor = "current_thread")] pub async fn get_positions() -> Result> { let positions = position::handler::get_positions()? diff --git a/mobile/native/src/db/custom_types.rs b/mobile/native/src/db/custom_types.rs index 4df3aa348..efa14cce0 100644 --- a/mobile/native/src/db/custom_types.rs +++ b/mobile/native/src/db/custom_types.rs @@ -4,6 +4,7 @@ use crate::db::models::Direction; use crate::db::models::FailureReason; use crate::db::models::Flow; use crate::db::models::HtlcStatus; +use crate::db::models::OrderReason; use crate::db::models::OrderState; use crate::db::models::OrderType; use crate::db::models::PositionState; @@ -40,6 +41,29 @@ impl FromSql for OrderType { } } +impl ToSql for OrderReason { + fn to_sql(&self, out: &mut Output) -> serialize::Result { + let text = match *self { + OrderReason::Manual => "Manual".to_string(), + OrderReason::Expired => "Expired".to_string(), + }; + out.set_value(text); + Ok(IsNull::No) + } +} + +impl FromSql for OrderReason { + fn from_sql(bytes: backend::RawValue) -> deserialize::Result { + let string = >::from_sql(bytes)?; + + return match string.as_str() { + "Manual" => Ok(OrderReason::Manual), + "Expired" => Ok(OrderReason::Expired), + _ => Err("Unrecognized enum variant".into()), + }; + } +} + impl ToSql for OrderState { fn to_sql(&self, out: &mut Output) -> serialize::Result { let text = match *self { diff --git a/mobile/native/src/db/mod.rs b/mobile/native/src/db/mod.rs index 84527abf8..6e040f54f 100644 --- a/mobile/native/src/db/mod.rs +++ b/mobile/native/src/db/mod.rs @@ -147,6 +147,21 @@ pub fn get_orders_for_ui() -> Result> { .collect::>()?) } +pub fn get_async_order() -> Result> { + let mut db = connection()?; + let order = Order::get_async_order(&mut db)?; + + let order: Option = match order { + Some(order) => { + let order = order.try_into()?; + Some(order) + } + None => None, + }; + + Ok(order) +} + pub fn get_filled_orders() -> Result> { let mut db = connection()?; diff --git a/mobile/native/src/db/models.rs b/mobile/native/src/db/models.rs index 0e837a49a..26ab7317f 100644 --- a/mobile/native/src/db/models.rs +++ b/mobile/native/src/db/models.rs @@ -125,6 +125,7 @@ pub(crate) struct Order { pub execution_price: Option, pub failure_reason: Option, pub order_expiry_timestamp: i64, + pub reason: OrderReason, } impl Order { @@ -221,6 +222,19 @@ impl Order { .load(conn) } + /// Gets any force order in the database. A force order is defined by any order with an reason + /// not equal to `Manual` + pub fn get_async_order(conn: &mut SqliteConnection) -> QueryResult> { + orders::table + .filter( + orders::state + .eq(OrderState::Filling) + .and(orders::reason.eq(OrderReason::Expired)), + ) + .first(conn) + .optional() + } + pub fn get_by_state( order_state: OrderState, conn: &mut SqliteConnection, @@ -256,6 +270,25 @@ impl From for Order { execution_price, failure_reason, order_expiry_timestamp: value.order_expiry_timestamp.unix_timestamp(), + reason: value.reason.into(), + } + } +} + +impl From for OrderReason { + fn from(value: crate::trade::order::OrderReason) -> Self { + match value { + crate::trade::order::OrderReason::Manual => OrderReason::Manual, + crate::trade::order::OrderReason::Expired => OrderReason::Expired, + } + } +} + +impl From for crate::trade::order::OrderReason { + fn from(value: OrderReason) -> Self { + match value { + OrderReason::Manual => crate::trade::order::OrderReason::Manual, + OrderReason::Expired => crate::trade::order::OrderReason::Expired, } } } @@ -278,6 +311,7 @@ impl TryFrom for crate::trade::order::Order { value.order_expiry_timestamp, ) .expect("unix timestamp to fit in itself"), + reason: value.reason.into(), }; Ok(order) @@ -509,6 +543,13 @@ impl TryFrom<(OrderType, Option)> for crate::trade::order::OrderType { } } +#[derive(Debug, Clone, Copy, PartialEq, FromSqlRow, AsExpression)] +#[diesel(sql_type = Text)] +pub enum OrderReason { + Manual, + Expired, +} + #[derive(Debug, Clone, Copy, PartialEq, FromSqlRow, AsExpression)] #[diesel(sql_type = Text)] pub enum OrderState { @@ -1319,6 +1360,7 @@ pub mod test { execution_price, failure_reason, order_expiry_timestamp: expiry_timestamp.unix_timestamp(), + reason: OrderReason::Manual, }; Order::insert( @@ -1332,6 +1374,7 @@ pub mod test { state: crate::trade::order::OrderState::Initial, creation_timestamp, order_expiry_timestamp: expiry_timestamp, + reason: crate::trade::order::OrderReason::Manual, } .into(), &mut connection, @@ -1350,6 +1393,7 @@ pub mod test { state: crate::trade::order::OrderState::Initial, creation_timestamp, order_expiry_timestamp: expiry_timestamp, + reason: crate::trade::order::OrderReason::Manual, } .into(), &mut connection, @@ -1417,6 +1461,7 @@ pub mod test { state: crate::trade::order::OrderState::Initial, creation_timestamp, order_expiry_timestamp, + reason: crate::trade::order::OrderReason::Manual, } .into(), &mut connection, @@ -1438,6 +1483,7 @@ pub mod test { state: crate::trade::order::OrderState::Initial, creation_timestamp, order_expiry_timestamp, + reason: crate::trade::order::OrderReason::Manual, } .into(), &mut connection, diff --git a/mobile/native/src/event/api.rs b/mobile/native/src/event/api.rs index 41638bad8..61f7330e3 100644 --- a/mobile/native/src/event/api.rs +++ b/mobile/native/src/event/api.rs @@ -5,6 +5,7 @@ use crate::event::EventType; use crate::health::ServiceUpdate; use crate::ln_dlc::ChannelStatus; use crate::trade::order::api::Order; +use crate::trade::order::api::OrderReason; use crate::trade::position::api::Position; use core::convert::From; use flutter_rust_bridge::frb; @@ -24,6 +25,7 @@ pub enum Event { PriceUpdateNotification(BestPrice), ServiceHealthUpdate(ServiceUpdate), ChannelStatusUpdate(ChannelStatus), + AsyncTrade(OrderReason), } impl From for Event { @@ -62,6 +64,7 @@ impl From for Event { EventInternal::PaymentClaimed(_) => { unreachable!("This internal event is not exposed to the UI") } + EventInternal::AsyncTrade(reason) => Event::AsyncTrade(reason.into()), } } } @@ -97,6 +100,7 @@ impl Subscriber for FlutterSubscriber { EventType::PriceUpdateNotification, EventType::ServiceHealthUpdate, EventType::ChannelStatusUpdate, + EventType::AsyncTrade, ] } } diff --git a/mobile/native/src/event/mod.rs b/mobile/native/src/event/mod.rs index 3a01273a5..1bf2ec3c0 100644 --- a/mobile/native/src/event/mod.rs +++ b/mobile/native/src/event/mod.rs @@ -4,6 +4,7 @@ use crate::event::subscriber::Subscriber; use crate::health::ServiceUpdate; use crate::ln_dlc::ChannelStatus; use crate::trade::order::Order; +use crate::trade::order::OrderReason; use crate::trade::position::Position; use coordinator_commons::TradeParams; use ln_dlc_node::node::rust_dlc_manager::ChannelId; @@ -29,6 +30,7 @@ pub fn publish(event: &EventInternal) { pub enum EventInternal { Init(String), Log(String), + AsyncTrade(OrderReason), OrderUpdateNotification(Order), WalletInfoUpdateNotification(WalletInfo), OrderFilledWith(Box), @@ -56,6 +58,7 @@ impl fmt::Display for EventInternal { EventInternal::PaymentClaimed(_) => "PaymentClaimed", EventInternal::ServiceHealthUpdate(_) => "ServiceHealthUpdate", EventInternal::ChannelStatusUpdate(_) => "ChannelStatusUpdate", + EventInternal::AsyncTrade(_) => "AsyncTrade", } .fmt(f) } @@ -78,6 +81,7 @@ impl From for EventType { EventInternal::PaymentClaimed(_) => EventType::PaymentClaimed, EventInternal::ServiceHealthUpdate(_) => EventType::ServiceHealthUpdate, EventInternal::ChannelStatusUpdate(_) => EventType::ChannelStatusUpdate, + EventInternal::AsyncTrade(_) => EventType::AsyncTrade, } } } @@ -96,4 +100,5 @@ pub enum EventType { PaymentClaimed, ServiceHealthUpdate, ChannelStatusUpdate, + AsyncTrade, } diff --git a/mobile/native/src/ln_dlc/node.rs b/mobile/native/src/ln_dlc/node.rs index 53d6b5b76..9ea515bba 100644 --- a/mobile/native/src/ln_dlc/node.rs +++ b/mobile/native/src/ln_dlc/node.rs @@ -299,27 +299,13 @@ impl Node { .. })) = msg { - match order::handler::order_filled() { - Ok(filled_order) => { - position::handler::update_position_after_dlc_closure(filled_order) - .context("Failed to update position after DLC closure")?; + let filled_order = order::handler::order_filled()?; + position::handler::update_position_after_dlc_closure(filled_order) + .context("Failed to update position after DLC closure")?; - if let Err(e) = self.pay_order_matching_fee(&channel_id) { - tracing::error!("{e:#}"); - } - } - // TODO: Should we charge for the order-matching fee if there is no order???? - Err(e) => { - tracing::warn!("Could not find a filling position in the database. Maybe because the coordinator closed an expired position. Error: {e:#}"); - - tokio::spawn(async { - match position::handler::close_position().await { - Ok(_) => tracing::info!("Successfully closed expired position."), - Err(e) => tracing::error!("Critical Error! We have a DLC but were unable to set the order to filled. Error: {e:?}") - } - }); - } - }; + if let Err(e) = self.pay_order_matching_fee(&channel_id) { + tracing::error!("{e:#}"); + } }; Ok(()) diff --git a/mobile/native/src/orderbook.rs b/mobile/native/src/orderbook.rs index eb75cab68..1b6fecfb7 100644 --- a/mobile/native/src/orderbook.rs +++ b/mobile/native/src/orderbook.rs @@ -1,4 +1,6 @@ use crate::config; +use crate::event; +use crate::event::EventInternal; use crate::health::ServiceStatus; use crate::trade::position; use anyhow::Result; @@ -101,11 +103,19 @@ pub fn subscribe( }; match msg { + OrderbookMsg::AsyncMatch { order, filled_with } => { + tracing::info!(order_id = %order.id, "Received an async match from orderbook. Reason: {:?}", order.order_reason); + event::publish(&EventInternal::AsyncTrade(order.clone().order_reason.into())); + + if let Err(e) = position::handler::force_trade(order.clone(), filled_with).await { + tracing::error!(order_id = %order.id, "Failed to process force trade. Error: {e:#}"); + } + }, OrderbookMsg::Match(filled) => { tracing::info!(order_id = %filled.order_id, "Received match from orderbook"); - if let Err(e) = position::handler::trade(filled).await { - tracing::error!("Trade request sent to coordinator failed. Error: {e:#}"); + if let Err(e) = position::handler::trade(filled.clone()).await { + tracing::error!(order_id = %filled.order_id, "Trade request sent to coordinator failed. Error: {e:#}"); } }, OrderbookMsg::AllOrders(initial_orders) => { diff --git a/mobile/native/src/schema.rs b/mobile/native/src/schema.rs index c65902860..8a93652be 100644 --- a/mobile/native/src/schema.rs +++ b/mobile/native/src/schema.rs @@ -35,6 +35,7 @@ diesel::table! { execution_price -> Nullable, failure_reason -> Nullable, order_expiry_timestamp -> BigInt, + reason -> Text, } } diff --git a/mobile/native/src/trade/order/api.rs b/mobile/native/src/trade/order/api.rs index 497b824b7..2b4c469a0 100644 --- a/mobile/native/src/trade/order/api.rs +++ b/mobile/native/src/trade/order/api.rs @@ -23,6 +23,13 @@ pub enum OrderState { Filled, } +#[frb] +#[derive(Debug, Clone, Copy)] +pub enum OrderReason { + Manual, + Expired, +} + #[frb] #[derive(Debug, Clone)] pub struct NewOrder { @@ -53,6 +60,7 @@ pub struct Order { pub execution_price: Option, pub creation_timestamp: i64, pub order_expiry_timestamp: i64, + pub reason: OrderReason, } impl From for OrderType { @@ -82,6 +90,25 @@ impl From for Order { execution_price, creation_timestamp: value.creation_timestamp.unix_timestamp(), order_expiry_timestamp: value.order_expiry_timestamp.unix_timestamp(), + reason: value.reason.into(), + } + } +} + +impl From for order::OrderReason { + fn from(value: OrderReason) -> Self { + match value { + OrderReason::Manual => order::OrderReason::Manual, + OrderReason::Expired => order::OrderReason::Expired, + } + } +} + +impl From for OrderReason { + fn from(value: order::OrderReason) -> Self { + match value { + order::OrderReason::Manual => OrderReason::Manual, + order::OrderReason::Expired => OrderReason::Expired, } } } @@ -126,6 +153,7 @@ impl From for order::Order { creation_timestamp: OffsetDateTime::now_utc(), // We do not support setting order expiry from the frontend for now order_expiry_timestamp: OffsetDateTime::now_utc() + time::Duration::minutes(1), + reason: order::OrderReason::Manual, } } } diff --git a/mobile/native/src/trade/order/handler.rs b/mobile/native/src/trade/order/handler.rs index bedf93b8d..8126f22b0 100644 --- a/mobile/native/src/trade/order/handler.rs +++ b/mobile/native/src/trade/order/handler.rs @@ -112,6 +112,10 @@ pub async fn get_orders_for_ui() -> Result> { db::get_orders_for_ui() } +pub fn get_async_order() -> Result> { + db::get_async_order() +} + fn get_order_being_filled() -> Result { let order_being_filled = match db::maybe_get_order_in_filling() { Ok(Some(order_being_filled)) => order_being_filled, diff --git a/mobile/native/src/trade/order/mod.rs b/mobile/native/src/trade/order/mod.rs index cfba78e50..df5e2d6ca 100644 --- a/mobile/native/src/trade/order/mod.rs +++ b/mobile/native/src/trade/order/mod.rs @@ -103,6 +103,30 @@ pub enum OrderState { }, } +#[derive(Debug, Clone, Copy)] +pub enum OrderReason { + Manual, + Expired, +} + +impl From for orderbook_commons::OrderReason { + fn from(value: OrderReason) -> Self { + match value { + OrderReason::Manual => orderbook_commons::OrderReason::Manual, + OrderReason::Expired => orderbook_commons::OrderReason::Expired, + } + } +} + +impl From for OrderReason { + fn from(value: orderbook_commons::OrderReason) -> Self { + match value { + orderbook_commons::OrderReason::Manual => OrderReason::Manual, + orderbook_commons::OrderReason::Expired => OrderReason::Expired, + } + } +} + #[derive(Debug, Clone, Copy)] pub struct Order { pub id: Uuid, @@ -114,6 +138,7 @@ pub struct Order { pub state: OrderState, pub creation_timestamp: OffsetDateTime, pub order_expiry_timestamp: OffsetDateTime, + pub reason: OrderReason, } impl Order { diff --git a/mobile/native/src/trade/position/handler.rs b/mobile/native/src/trade/position/handler.rs index 790046687..b0c7b59b7 100644 --- a/mobile/native/src/trade/position/handler.rs +++ b/mobile/native/src/trade/position/handler.rs @@ -1,5 +1,4 @@ use crate::calculations::calculate_liquidation_price; -use crate::config::get_network; use crate::db; use crate::event; use crate::event::EventInternal; @@ -20,9 +19,7 @@ use orderbook_commons::Prices; use rust_decimal::prelude::ToPrimitive; use time::Duration; use time::OffsetDateTime; -use trade::bitmex_client::BitmexClient; use trade::ContractSymbol; -use uuid::Uuid; /// Sets up a trade with the counterparty /// @@ -58,6 +55,54 @@ pub async fn trade(filled: FilledWith) -> Result<()> { Ok(()) } +/// Executes a force trade from the orderbook / coordinator. e.g. this will happen if the position +/// expires. +pub async fn force_trade(order: orderbook_commons::Order, filled_with: FilledWith) -> Result<()> { + let order_type = match order.order_type { + orderbook_commons::OrderType::Market => OrderType::Market, + orderbook_commons::OrderType::Limit => OrderType::Limit { + price: order.price.to_f32().expect("to fit into f32"), + }, + }; + + let execution_price = filled_with + .average_execution_price() + .to_f32() + .expect("to fit into f32"); + let order = Order { + id: order.id, + leverage: order.leverage, + quantity: order.quantity.to_f32().expect("to fit into f32"), + contract_symbol: order.contract_symbol, + direction: order.direction, + order_type, + state: OrderState::Filling { execution_price }, + creation_timestamp: order.timestamp, + order_expiry_timestamp: order.expiry, + reason: order.order_reason.into(), + }; + + db::insert_order(order)?; + + event::publish(&EventInternal::OrderUpdateNotification(order)); + + let trade_params = TradeParams { + pubkey: ln_dlc::get_node_info().pubkey, + contract_symbol: order.contract_symbol, + leverage: order.leverage, + quantity: order.quantity, + direction: order.direction, + filled_with, + }; + + if let Err((reason, e)) = ln_dlc::trade(trade_params).await { + order::handler::order_failed(Some(order.id), reason, e) + .context("Could not set order to failed")?; + } + + Ok(()) +} + /// Fetch the positions from the database pub fn get_positions() -> Result> { db::get_positions() @@ -180,45 +225,3 @@ pub fn price_update(prices: Prices) -> Result<()> { event::publish(&EventInternal::PriceUpdateNotification(prices)); Ok(()) } - -// FIXME: This is not ideal, but closing the position after -// the position has expired is not triggered by the app -// through an order. Instead the coordinator simply proposes -// a close position. In order to fixup the ui, we are -// creating an order here and store it to the database. -pub async fn close_position() -> Result<()> { - let positions = get_positions()? - .into_iter() - .filter(|p| p.position_state == PositionState::Open) - .map(Position::from) - .collect::>(); - - let position = positions - .first() - .context("Exactly one positions should be open when trying to close a position")?; - - tracing::debug!("Adding order for the expired closed position"); - - let quote = BitmexClient::get_quote(&get_network(), &position.expiry).await?; - let closing_price = quote.get_price_for_direction(position.direction.opposite()); - - let order = Order { - id: Uuid::new_v4(), - leverage: position.leverage, - quantity: position.quantity, - contract_symbol: position.contract_symbol, - direction: position.direction.opposite(), - order_type: OrderType::Market, - state: OrderState::Filled { - execution_price: closing_price.to_f32().expect("to fit into f32"), - }, - creation_timestamp: OffsetDateTime::now_utc(), - // position has already expired, so the order expiry doesn't matter - order_expiry_timestamp: OffsetDateTime::now_utc() + Duration::minutes(1), - }; - - event::publish(&EventInternal::OrderUpdateNotification(order)); - - db::insert_order(order)?; - update_position_after_dlc_closure(order) -} diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 9e8afc302..a405f7860 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -181,10 +181,10 @@ packages: dependency: transitive description: name: collection - sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" url: "https://pub.dev" source: hosted - version: "1.17.2" + version: "1.17.1" colorize: dependency: transitive description: @@ -604,18 +604,18 @@ packages: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.15" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.2.0" meta: dependency: "direct main" description: @@ -1017,10 +1017,10 @@ packages: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.9.1" stack_trace: dependency: transitive description: @@ -1073,26 +1073,26 @@ packages: dependency: "direct main" description: name: test - sha256: "13b41f318e2a5751c3169137103b60c584297353d4b1761b66029bae6411fe46" + sha256: "3dac9aecf2c3991d09b9cdde4f98ded7b30804a88a0d7e4e7e1678e78d6b97f4" url: "https://pub.dev" source: hosted - version: "1.24.3" + version: "1.24.1" test_api: dependency: transitive description: name: test_api - sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.5.1" test_core: dependency: transitive description: name: test_core - sha256: "99806e9e6d95c7b059b7a0fc08f07fc53fabe54a829497f0d9676299f1e8637e" + sha256: "5138dbffb77b2289ecb12b81c11ba46036590b72a64a7a90d6ffb880f1a29e93" url: "https://pub.dev" source: hosted - version: "0.5.3" + version: "0.5.1" timeago: dependency: "direct main" description: @@ -1237,14 +1237,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" - web: - dependency: transitive - description: - name: web - sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 - url: "https://pub.dev" - source: hosted - version: "0.1.4-beta" web_socket_channel: dependency: transitive description: @@ -1310,5 +1302,5 @@ packages: source: hosted version: "2.1.1" sdks: - dart: ">=3.1.0-185.0.dev <4.0.0" + dart: ">=3.0.0 <4.0.0" flutter: ">=3.7.0"