From cc80eb83e781d699e47234b0b9be7544a813e24b Mon Sep 17 00:00:00 2001 From: Richard Holzeis Date: Thu, 4 Apr 2024 10:58:40 +0200 Subject: [PATCH] chore: Rework maintenance margin for liquidation calculation Introduces the maintenance margin in replacement of the incorrect implementation of a margin call. Also introduces the bankruptcy liquidation price, which is a liquidation price with a maintenance margin of 0%. --- .../test-coordinator-settings.toml | 2 +- coordinator/src/node.rs | 2 +- coordinator/src/node/liquidated_positions.rs | 261 ++++++++++-------- coordinator/src/orderbook/websocket.rs | 6 +- coordinator/src/payout_curve.rs | 15 +- coordinator/src/settings.rs | 15 +- coordinator/src/trade/mod.rs | 65 ++--- crates/commons/src/message.rs | 2 +- .../payout_curve/examples/payout_curve_csv.rs | 16 +- crates/payout_curve/src/lib.rs | 16 +- .../tests/integration_proptests.rs | 20 +- crates/trade/src/cfd.rs | 24 +- .../lib/common/domain/tentenone_config.dart | 2 +- .../trade/trade_bottom_sheet_tab.dart | 16 +- mobile/native/src/api.rs | 10 +- mobile/native/src/calculations/mod.rs | 15 +- .../native/src/channel_trade_constraints.rs | 12 +- mobile/native/src/emergency_kit.rs | 10 +- mobile/native/src/lib.rs | 2 + mobile/native/src/ln_dlc/mod.rs | 8 + mobile/native/src/trade/position/mod.rs | 16 +- mobile/test/trade_test.dart | 4 +- 22 files changed, 299 insertions(+), 240 deletions(-) diff --git a/coordinator/example-settings/test-coordinator-settings.toml b/coordinator/example-settings/test-coordinator-settings.toml index dc9310a55..7b755b0eb 100644 --- a/coordinator/example-settings/test-coordinator-settings.toml +++ b/coordinator/example-settings/test-coordinator-settings.toml @@ -8,7 +8,7 @@ whitelist_enabled = false # Default testnet maker whitelisted_makers = ["035eccdd1f05c65b433cf38e3b2597e33715e0392cb14d183e812f1319eb7b6794"] min_quantity = 1 -margin_call_percentage = 0.1 +maintenance_margin = 0.1 [ln_dlc] off_chain_sync_interval = 5 diff --git a/coordinator/src/node.rs b/coordinator/src/node.rs index 9dca663ea..f9c0afc4a 100644 --- a/coordinator/src/node.rs +++ b/coordinator/src/node.rs @@ -46,7 +46,7 @@ pub struct NodeSettings { // At times, we want to disallow opening new positions (e.g. before // scheduled upgrade) pub allow_opening_positions: bool, - pub margin_call_percentage: f32, + pub maintenance_margin: f32, } #[derive(Clone)] diff --git a/coordinator/src/node/liquidated_positions.rs b/coordinator/src/node/liquidated_positions.rs index 402b4cd5b..e761f23b8 100644 --- a/coordinator/src/node/liquidated_positions.rs +++ b/coordinator/src/node/liquidated_positions.rs @@ -34,9 +34,9 @@ pub async fn monitor(node: Node, trading_sender: mpsc::Sender) } } -/// Checks all open positions if given the best price the margin call has been reached and the -/// position needs to get liquidated. If so an async match is created and the user is notified about -/// the pending liquidation. +/// Checks all open positions if given the best price the maintenance margin has been reached and +/// the position needs to get liquidated. If so an async match is created and the user is notified +/// about the pending liquidation. async fn check_if_positions_need_to_get_liquidated( trading_sender: mpsc::Sender, node: Node, @@ -62,12 +62,18 @@ async fn check_if_positions_need_to_get_liquidated( let trader_liquidation_price = Decimal::try_from(position.trader_liquidation_price).expect("to fit into decimal"); - if let Some(direction) = check_if_position_needs_to_get_liquidated( + let trader_liquidation = check_if_position_needs_to_get_liquidated( position.trader_direction, best_current_price, trader_liquidation_price, + ); + let coordinator_liquidation = check_if_position_needs_to_get_liquidated( + position.trader_direction.opposite(), + best_current_price, coordinator_liquidation_price, - ) { + ); + + if trader_liquidation || coordinator_liquidation { if let Some(order) = orderbook::db::orders::get_by_trader_id_and_state( &mut conn, position.trader, @@ -109,7 +115,7 @@ async fn check_if_positions_need_to_get_liquidated( } } - tracing::info!(trader_id=%position.trader, ?best_current_price, %direction, position_id=%position.id, "Attempting to close liquidated position"); + tracing::info!(trader_id=%position.trader, ?best_current_price, position_id=%position.id, "Attempting to close liquidated position"); // Ensure that the users channel is confirmed on-chain before continuing with the // liquidation. @@ -172,56 +178,21 @@ async fn check_if_positions_need_to_get_liquidated( Ok(()) } -/// Checks if the position needs to get liquidated. Either from the trader or the coordinator point -/// of view. fn check_if_position_needs_to_get_liquidated( - trader_direction: Direction, + direction: Direction, best_current_price: &Price, - trader_liquidation_price: Decimal, - coordinator_liquidation_price: Decimal, -) -> Option { - match trader_direction { - Direction::Short => { - // if the trader is short that means the coordinator is long, so we have to check the - // coordinators liquidation price on the bid price. - if let Some(bid) = best_current_price.bid { - // check if coordinator needs to get liquidated - if bid <= coordinator_liquidation_price { - return Some(Direction::Long); - } - } - - // if the trader is short that means the coordinator is long, so we have to check the - // traders liquidation price on the ask price. - if let Some(ask) = best_current_price.ask { - // check if trader needs to get liquidated - if ask >= trader_liquidation_price { - return Some(Direction::Short); - } - } - } - Direction::Long => { - // if the trader is long that means the coordinator is short, so we have to check the - // traders liquidation price on the bid price. - if let Some(bid) = best_current_price.bid { - // check if trader needs to get liquidated - if bid <= trader_liquidation_price { - return Some(Direction::Long); - } - } - - // if the trader is long that means the coordinator is short, so we have to check the - // coordinators liquidation price on the ask price. - if let Some(ask) = best_current_price.ask { - // check if coordinator needs to get liquidated - if ask >= coordinator_liquidation_price { - return Some(Direction::Short); - } - } - } + liquidation_price: Decimal, +) -> bool { + match direction { + Direction::Short => best_current_price + .ask + .map(|ask| ask >= liquidation_price) + .unwrap_or(false), + Direction::Long => best_current_price + .bid + .map(|bid| bid <= liquidation_price) + .unwrap_or(false), } - - None } #[cfg(test)] @@ -232,7 +203,7 @@ mod tests { use trade::Direction; #[test] - fn test_no_liquidatation_of_users_short_position_before_margin_call() { + fn test_no_liquidatation_of_users_short_position_before_maintenance_margin() { let trader_direction = Direction::Short; let price = Price { ask: Some(Decimal::from(33749)), @@ -243,18 +214,23 @@ mod tests { trader_direction, &price, // the liquidation price of the trader is at 30,000 * 5 / 4 = 37,500 - // margin call is at 10% of the liquidation price = 37,500 - 3,750 = 33,750 + // maintenance margin is at 10% of the liquidation price = 37,500 - 3,750 = 33,750 Decimal::from(33750), + ); + assert!(!liquidation); + + let liquidation = check_if_position_needs_to_get_liquidated( + trader_direction.opposite(), + &price, // the liquidation price of the coordinator is at 30,000 * 5 / 6 = 25,000 - // margin call is at 10% of the liquidation price = 25,000 + 2,500 = 27,500 + // maintenance margin is at 10% of the liquidation price = 25,000 + 2,500 = 27,500 Decimal::from(27500), ); - - assert_eq!(None, liquidation); + assert!(!liquidation); } #[test] - fn test_liquidate_users_short_position_at_margin_call() { + fn test_liquidate_users_short_position_at_maintenance_margin() { let trader_direction = Direction::Short; let price = Price { ask: Some(Decimal::from(33750)), @@ -265,18 +241,23 @@ mod tests { trader_direction, &price, // the liquidation price of the trader is at 30,000 * 5 / 4 = 37,500 - // margin call is at 10% of the liquidation price = 37,500 - 3,750 = 33,750 + // maintenance margin is at 10% of the liquidation price = 37,500 - 3,750 = 33,750 Decimal::from(33750), + ); + assert!(liquidation); + + let liquidation = check_if_position_needs_to_get_liquidated( + trader_direction.opposite(), + &price, // the liquidation price of the coordinator is at 30,000 * 5 / 6 = 25,000 - // margin call is at 10% of the liquidation price = 25,000 + 2,500 = 27,500 + // maintenance margin is at 10% of the liquidation price = 25,000 + 2,500 = 27,500 Decimal::from(27500), ); - - assert_eq!(Some(Direction::Short), liquidation); + assert!(!liquidation); } #[test] - fn test_liquidate_users_short_position_after_margin_call() { + fn test_liquidate_users_short_position_after_maintenance_margin() { let trader_direction = Direction::Short; let price = Price { ask: Some(Decimal::from(33751)), @@ -287,18 +268,23 @@ mod tests { trader_direction, &price, // the liquidation price of the trader is at 30,000 * 5 / 4 = 37,500 - // margin call is at 10% of the liquidation price = 37,500 - 3,750 = 33,750 + // maintenance margin is at 10% of the liquidation price = 37,500 - 3,750 = 33,750 Decimal::from(33750), + ); + assert!(liquidation); + + let liquidation = check_if_position_needs_to_get_liquidated( + trader_direction.opposite(), + &price, // the liquidation price of the coordinator is at 30,000 * 5 / 6 = 25,000 - // margin call is at 10% of the liquidation price = 25,000 + 2,500 = 27,500 + // maintenance margin is at 10% of the liquidation price = 25,000 + 2,500 = 27,500 Decimal::from(27500), ); - - assert_eq!(Some(Direction::Short), liquidation); + assert!(!liquidation); } #[test] - fn test_no_liquidation_of_users_long_position_before_margin_call() { + fn test_no_liquidation_of_users_long_position_before_maintenance_margin() { let trader_direction = Direction::Long; let price = Price { ask: None, @@ -309,18 +295,23 @@ mod tests { trader_direction, &price, // the liquidation price of the trader is at 30,000 * 5 / 6 = 25,000 - // margin call is at 10% of the liquidation price = 25,000 + 2,500 = 27,500 + // maintenance margin is at 10% of the liquidation price = 25,000 + 2,500 = 27,500 Decimal::from(27500), + ); + assert!(!liquidation); + + let liquidation = check_if_position_needs_to_get_liquidated( + trader_direction.opposite(), + &price, // the liquidation price of the coordinator is at 30,000 * 5 / 4 = 37,500 - // margin call is at 10% of the liquidation price = 37,500 - 3,750 = 33,750 + // maintenance margin is at 10% of the liquidation price = 37,500 - 3,750 = 33,750 Decimal::from(33750), ); - - assert_eq!(None, liquidation); + assert!(!liquidation); } #[test] - fn test_liquidate_users_long_position_at_margin_call() { + fn test_liquidate_users_long_position_at_maintenance_margin() { let trader_direction = Direction::Long; let price = Price { ask: None, @@ -331,18 +322,23 @@ mod tests { trader_direction, &price, // the liquidation price of the trader is at 30,000 * 5 / 6 = 25,000 - // margin call is at 10% of the liquidation price = 25,000 + 2,500 = 27,500 + // maintenance margin is at 10% of the liquidation price = 25,000 + 2,500 = 27,500 Decimal::from(27500), + ); + assert!(liquidation); + + let liquidation = check_if_position_needs_to_get_liquidated( + trader_direction.opposite(), + &price, // the liquidation price of the coordinator is at 30,000 * 5 / 4 = 37,500 - // margin call is at 10% of the liquidation price = 37,500 - 3,750 = 33,750 + // maintenance margin is at 10% of the liquidation price = 37,500 - 3,750 = 33,750 Decimal::from(33750), ); - - assert_eq!(Some(Direction::Long), liquidation); + assert!(!liquidation); } #[test] - fn test_liquidate_users_long_position_after_margin_call() { + fn test_liquidate_users_long_position_after_maintenance_margin() { let trader_direction = Direction::Long; let price = Price { ask: None, @@ -353,18 +349,23 @@ mod tests { trader_direction, &price, // the liquidation price of the trader is at 30,000 * 5 / 6 = 25,000 - // margin call is at 10% of the liquidation price = 25,000 + 2,500 = 27,500 + // maintenance margin is at 10% of the liquidation price = 25,000 + 2,500 = 27,500 Decimal::from(27500), + ); + assert!(liquidation); + + let liquidation = check_if_position_needs_to_get_liquidated( + trader_direction.opposite(), + &price, // the liquidation price of the coordinator is at 30,000 * 5 / 4 = 37,500 - // margin call is at 10% of the liquidation price = 37,500 - 3,750 = 33,750 + // maintenance margin is at 10% of the liquidation price = 37,500 - 3,750 = 33,750 Decimal::from(33750), ); - - assert_eq!(Some(Direction::Long), liquidation); + assert!(!liquidation); } #[test] - fn test_liquidate_coordinators_short_position_at_margin_call() { + fn test_liquidate_coordinators_short_position_at_maintenance_margin() { let trader_direction = Direction::Long; let price = Price { ask: Some(Decimal::from(54000)), @@ -375,18 +376,23 @@ mod tests { trader_direction, &price, // the liquidation price of the trader is at 30,000 * 2 / 3 = 20,000 - // margin call is at 10% of the liquidation price = 20,000 + 2,000 = 22,000 + // maintenance margin is at 10% of the liquidation price = 20,000 + 2,000 = 22,000 Decimal::from(22000), + ); + assert!(!liquidation); + + let liquidation = check_if_position_needs_to_get_liquidated( + trader_direction.opposite(), + &price, // the liquidation price of the coordinator is at 30,000 * 2 / 1 = 60,000 - // margin call is at 10% of the liquidation price = 60,000 - 6,000 = 54,000 + // maintenance margin is at 10% of the liquidation price = 60,000 - 6,000 = 54,000 Decimal::from(54000), ); - - assert_eq!(Some(Direction::Short), liquidation); + assert!(liquidation); } #[test] - fn test_liquidate_coordinators_short_position_after_margin_call() { + fn test_liquidate_coordinators_short_position_after_maintenance_margin() { let trader_direction = Direction::Long; let price = Price { ask: Some(Decimal::from(54001)), @@ -397,97 +403,126 @@ mod tests { trader_direction, &price, // the liquidation price of the trader is at 30,000 * 2 / 3 = 20,000 - // margin call is at 10% of the liquidation price = 20,000 + 2,000 = 22,000 + // maintenance margin is at 10% of the liquidation price = 20,000 + 2,000 = 22,000 Decimal::from(22000), + ); + assert!(!liquidation); + + let liquidation = check_if_position_needs_to_get_liquidated( + trader_direction.opposite(), + &price, // the liquidation price of the coordinator is at 30,000 * 2 / 1 = 60,000 - // margin call is at 10% of the liquidation price = 60,000 - 6,000 = 54,000 + // maintenance margin is at 10% of the liquidation price = 60,000 - 6,000 = 54,000 Decimal::from(54000), ); - - assert_eq!(Some(Direction::Short), liquidation); + assert!(liquidation); } #[test] - fn test_no_liquidation_of_coordinators_short_position_before_margin_call() { + fn test_no_liquidation_of_coordinators_short_position_before_maintenance_margin() { let trader_direction = Direction::Long; let price = Price { ask: None, bid: Some(Decimal::from(53999)), }; + let liquidation = check_if_position_needs_to_get_liquidated( trader_direction, &price, // the liquidation price of the trader is at 30,000 * 2 / 3 = 20,000 - // margin call is at 10% of the liquidation price = 20,000 + 2,000 = 22,000 + // maintenance margin is at 10% of the liquidation price = 20,000 + 2,000 = 22,000 Decimal::from(22000), + ); + assert!(!liquidation); + + let liquidation = check_if_position_needs_to_get_liquidated( + trader_direction.opposite(), + &price, // the liquidation price of the coordinator is at 30,000 * 2 / 1 = 60,000 - // margin call is at 10% of the liquidation price = 60,000 - 6,000 = 54,000 + // maintenance margin is at 10% of the liquidation price = 60,000 - 6,000 = 54,000 Decimal::from(54000), ); - - assert_eq!(None, liquidation); + assert!(!liquidation); } #[test] - fn test_liquidate_coordinators_long_position_at_margin_call() { + fn test_liquidate_coordinators_long_position_at_maintenance_margin() { let trader_direction = Direction::Short; let price = Price { ask: None, bid: Some(Decimal::from(22000)), }; + let liquidation = check_if_position_needs_to_get_liquidated( trader_direction, &price, // the liquidation price of the trader is at 30,000 * 2 / 1 = 60,000 - // margin call is at 10% of the liquidation price = 60,000 - 6,000 = 54,000 + // maintenance margin is at 10% of the liquidation price = 60,000 - 6,000 = 54,000 Decimal::from(54000), + ); + assert!(!liquidation); + + let liquidation = check_if_position_needs_to_get_liquidated( + trader_direction.opposite(), + &price, // the liquidation price of the coordinator is at 30,000 * 2 / 3 = 20,000 - // margin call is at 10% of the liquidation price = 20,000 + 2,000 = 22,000 + // maintenance margin is at 10% of the liquidation price = 20,000 + 2,000 = 22,000 Decimal::from(22000), ); - - assert_eq!(Some(Direction::Long), liquidation); + assert!(liquidation); } #[test] - fn test_liquidate_coordinators_long_position_after_margin_call() { + fn test_liquidate_coordinators_long_position_after_maintenance_margin() { let trader_direction = Direction::Short; let price = Price { ask: None, bid: Some(Decimal::from(21999)), }; + let liquidation = check_if_position_needs_to_get_liquidated( trader_direction, &price, // the liquidation price of the trader is at 30,000 * 2 / 1 = 60,000 - // margin call is at 10% of the liquidation price = 60,000 - 6,000 = 54,000 + // maintenance margin is at 10% of the liquidation price = 60,000 - 6,000 = 54,000 Decimal::from(54000), + ); + assert!(!liquidation); + + let liquidation = check_if_position_needs_to_get_liquidated( + trader_direction.opposite(), + &price, // the liquidation price of the coordinator is at 30,000 * 2 / 3 = 20,000 - // margin call is at 10% of the liquidation price = 20,000 + 2,000 = 22,000 + // maintenance margin is at 10% of the liquidation price = 20,000 + 2,000 = 22,000 Decimal::from(22000), ); - - assert_eq!(Some(Direction::Long), liquidation); + assert!(liquidation); } #[test] - fn test_no_liquidation_of_coordinators_long_position_before_margin_call() { + fn test_no_liquidation_of_coordinators_long_position_before_maintenance_margin() { let trader_direction = Direction::Short; let price = Price { ask: None, bid: Some(Decimal::from(22001)), }; + let liquidation = check_if_position_needs_to_get_liquidated( trader_direction, &price, // the liquidation price of the trader is at 30,000 * 2 / 1 = 60,000 - // margin call is at 10% of the liquidation price = 60,000 - 6,000 = 54,000 + // maintenance margin is at 10% of the liquidation price = 60,000 - 6,000 = 54,000 Decimal::from(54000), + ); + assert!(!liquidation); + + let liquidation = check_if_position_needs_to_get_liquidated( + trader_direction.opposite(), + &price, // the liquidation price of the coordinator is at 30,000 * 2 / 3 = 20,000 - // margin call is at 10% of the liquidation price = 20,000 + 2,000 = 22,000 + // maintenance margin is at 10% of the liquidation price = 20,000 + 2,000 = 22,000 Decimal::from(22000), ); - - assert_eq!(None, liquidation); + assert!(!liquidation); } } diff --git a/coordinator/src/orderbook/websocket.rs b/coordinator/src/orderbook/websocket.rs index f0a220cf1..a70358b2d 100644 --- a/coordinator/src/orderbook/websocket.rs +++ b/coordinator/src/orderbook/websocket.rs @@ -105,16 +105,16 @@ pub async fn websocket_connection(stream: WebSocket, state: Arc) { let liquidity_options = db::liquidity_options::get_all(&mut conn).unwrap_or_default(); - let (min_quantity, margin_call_percentage) = { + let (min_quantity, maintenance_margin) = { let settings = state.settings.read().await; - (settings.min_quantity, settings.margin_call_percentage) + (settings.min_quantity, settings.maintenance_margin) }; if let Err(e) = local_sender .send(Message::Authenticated(TenTenOneConfig { liquidity_options, min_quantity, - margin_call_percentage, + maintenance_margin, })) .await { diff --git a/coordinator/src/payout_curve.rs b/coordinator/src/payout_curve.rs index a30b19e83..e138f0ae7 100644 --- a/coordinator/src/payout_curve.rs +++ b/coordinator/src/payout_curve.rs @@ -14,8 +14,8 @@ use payout_curve::ROUNDING_PERCENT; use rust_decimal::prelude::FromPrimitive; use rust_decimal::Decimal; use tracing::instrument; -use trade::cfd::calculate_long_liquidation_price; -use trade::cfd::calculate_short_liquidation_price; +use trade::cfd::calculate_long_bankruptcy_price; +use trade::cfd::calculate_short_bankruptcy_price; use trade::cfd::BTCUSD_MAX_PRICE; use trade::ContractSymbol; use trade::Direction; @@ -168,7 +168,8 @@ fn build_inverse_payout_function( Ok((payout_function, rounding_intervals)) } -/// Returns the liquidation price for `(coordinator, maker)` +/// Returns the liquidation price for `(coordinator, maker)` with a maintenance margin of 0%. also +/// known as the bankruptcy price. fn get_liquidation_prices( initial_price: Decimal, coordinator_direction: Direction, @@ -177,12 +178,12 @@ fn get_liquidation_prices( ) -> (Decimal, Decimal) { let (coordinator_liquidation_price, trader_liquidation_price) = match coordinator_direction { Direction::Long => ( - calculate_long_liquidation_price(leverage_coordinator, initial_price), - calculate_short_liquidation_price(leverage_trader, initial_price), + calculate_long_bankruptcy_price(leverage_coordinator, initial_price), + calculate_short_bankruptcy_price(leverage_trader, initial_price), ), Direction::Short => ( - calculate_short_liquidation_price(leverage_coordinator, initial_price), - calculate_long_liquidation_price(leverage_trader, initial_price), + calculate_short_bankruptcy_price(leverage_coordinator, initial_price), + calculate_long_bankruptcy_price(leverage_trader, initial_price), ), }; (coordinator_liquidation_price, trader_liquidation_price) diff --git a/coordinator/src/settings.rs b/coordinator/src/settings.rs index f5eca2b1e..e7f033f66 100644 --- a/coordinator/src/settings.rs +++ b/coordinator/src/settings.rs @@ -66,9 +66,12 @@ pub struct Settings { /// A list of makers who are allowed to post limit orders. This is to prevent spam. pub whitelisted_makers: Vec, + /// The min quantity that we accept to be traded with. pub min_quantity: u64, - pub margin_call_percentage: f32, + /// The maintenance margin in percent, defining the required margin in the position. If the + /// margin drops below that the position gets liquidated. + pub maintenance_margin: f32, } impl Settings { @@ -102,7 +105,7 @@ impl Settings { pub fn to_node_settings(&self) -> NodeSettings { NodeSettings { allow_opening_positions: self.new_positions_enabled, - margin_call_percentage: self.margin_call_percentage, + maintenance_margin: self.maintenance_margin, } } @@ -123,7 +126,7 @@ impl Settings { whitelist_enabled: file.whitelist_enabled, whitelisted_makers: file.whitelisted_makers, min_quantity: file.min_quantity, - margin_call_percentage: file.margin_call_percentage, + maintenance_margin: file.maintenance_margin, } } } @@ -146,7 +149,7 @@ pub struct SettingsFile { whitelisted_makers: Vec, min_quantity: u64, - margin_call_percentage: f32, + maintenance_margin: f32, } impl From for SettingsFile { @@ -162,7 +165,7 @@ impl From for SettingsFile { whitelist_enabled: false, whitelisted_makers: value.whitelisted_makers, min_quantity: value.min_quantity, - margin_call_percentage: value.margin_call_percentage, + maintenance_margin: value.maintenance_margin, } } } @@ -194,7 +197,7 @@ mod tests { ) .unwrap()], min_quantity: 1, - margin_call_percentage: 0.1, + maintenance_margin: 0.1, }; let serialized = toml::to_string_pretty(&original).unwrap(); diff --git a/coordinator/src/trade/mod.rs b/coordinator/src/trade/mod.rs index 7b6005355..2026c4761 100644 --- a/coordinator/src/trade/mod.rs +++ b/coordinator/src/trade/mod.rs @@ -529,36 +529,24 @@ impl TradeExecutor { coordinator_leverage: f32, stable: bool, ) -> Result<()> { - let trader_liquidation_price = trader_liquidation_price(trade_params); - let price = trade_params.average_execution_price(); - let coordinator_liquidation_price = coordinator_liquidation_price( + let maintenance_margin = { self.node.settings.read().await.maintenance_margin }; + let maintenance_margin = + Decimal::try_from(maintenance_margin).expect("to fit into decimal"); + + let trader_liquidation_price = liquidation_price( price, Decimal::try_from(coordinator_leverage).expect("to fit into decimal"), - trade_params.direction.opposite(), + trade_params.direction, + maintenance_margin, ); - let coordinator_liquidation_price = - Decimal::try_from(coordinator_liquidation_price).expect("to fit into decimal"); - let trader_liquidation_price = - Decimal::try_from(trader_liquidation_price).expect("to fit into decimal"); - - let margin_call_percentage = { self.node.settings.read().await.margin_call_percentage }; - let margin_call_percentage = - Decimal::try_from(margin_call_percentage).expect("to fit into decimal"); - let (coordinator_liquidation_price, trader_liquidation_price) = match trade_params.direction - { - Direction::Short => ( - coordinator_liquidation_price - + coordinator_liquidation_price * margin_call_percentage, - trader_liquidation_price - trader_liquidation_price * margin_call_percentage, - ), - Direction::Long => ( - coordinator_liquidation_price - - coordinator_liquidation_price * margin_call_percentage, - trader_liquidation_price + trader_liquidation_price * margin_call_percentage, - ), - }; + let coordinator_liquidation_price = liquidation_price( + price, + Decimal::try_from(coordinator_leverage).expect("to fit into decimal"), + trade_params.direction.opposite(), + maintenance_margin, + ); let margin_coordinator = margin_coordinator(trade_params, coordinator_leverage); let margin_trader = margin_trader(trade_params); @@ -811,29 +799,20 @@ fn margin_coordinator(trade_params: &TradeParams, coordinator_leverage: f32) -> ) } -fn trader_liquidation_price(trade_params: &TradeParams) -> f32 { - let price = trade_params.average_execution_price(); - let leverage = Decimal::try_from(trade_params.leverage).expect("to fit into decimal"); - - match trade_params.direction { - Direction::Long => calculate_long_liquidation_price(leverage, price), - Direction::Short => calculate_short_liquidation_price(leverage, price), - } - .to_f32() - .expect("to fit into f32") -} - -fn coordinator_liquidation_price( +fn liquidation_price( price: Decimal, coordinator_leverage: Decimal, direction: Direction, -) -> f32 { + maintenance_margin: Decimal, +) -> Decimal { match direction { - Direction::Long => calculate_long_liquidation_price(coordinator_leverage, price), - Direction::Short => calculate_short_liquidation_price(coordinator_leverage, price), + Direction::Long => { + calculate_long_liquidation_price(coordinator_leverage, price, maintenance_margin) + } + Direction::Short => { + calculate_short_liquidation_price(coordinator_leverage, price, maintenance_margin) + } } - .to_f32() - .expect("to fit into f32") } pub fn coordinator_leverage_for_trade(_counterparty_peer_id: &PublicKey) -> Result { diff --git a/crates/commons/src/message.rs b/crates/commons/src/message.rs index 51c8b384e..733e66949 100644 --- a/crates/commons/src/message.rs +++ b/crates/commons/src/message.rs @@ -69,7 +69,7 @@ pub struct TenTenOneConfig { // The liquidity options for onboarding pub liquidity_options: Vec, pub min_quantity: u64, - pub margin_call_percentage: f32, + pub maintenance_margin: f32, } #[derive(Serialize, Clone, Deserialize, Debug)] diff --git a/crates/payout_curve/examples/payout_curve_csv.rs b/crates/payout_curve/examples/payout_curve_csv.rs index 461200b3e..23245691d 100644 --- a/crates/payout_curve/examples/payout_curve_csv.rs +++ b/crates/payout_curve/examples/payout_curve_csv.rs @@ -18,10 +18,10 @@ use rust_decimal::Decimal; use rust_decimal_macros::dec; use std::fs::File; use std::ops::Mul; -use trade::cfd::calculate_long_liquidation_price; +use trade::cfd::calculate_long_bankruptcy_price; use trade::cfd::calculate_margin; use trade::cfd::calculate_pnl; -use trade::cfd::calculate_short_liquidation_price; +use trade::cfd::calculate_short_bankruptcy_price; use trade::Direction; /// The example below will export the computed payout curve and how it should look like as CSV. @@ -35,12 +35,12 @@ fn main() -> Result<()> { let leverage_long = 2.0; let price_params = { - let short_liquidation_price = calculate_short_liquidation_price( + let short_liquidation_price = calculate_short_bankruptcy_price( Decimal::from_f32(leverage_short).expect("to be able to parse f32"), initial_price, ); - let long_liquidation_price = calculate_long_liquidation_price( + let long_liquidation_price = calculate_long_bankruptcy_price( Decimal::from_f32(leverage_long).expect("to be able to parse f32"), initial_price, ); @@ -289,8 +289,8 @@ pub fn should_payouts_as_csv_short( let file = File::create(csv_path)?; let mut wtr = csv::WriterBuilder::new().delimiter(b';').from_writer(file); - let long_liquidation_price = calculate_long_liquidation_price(leverage_long, initial_price); - let short_liquidation_price = calculate_short_liquidation_price(leverage_short, initial_price); + let long_liquidation_price = calculate_long_bankruptcy_price(leverage_long, initial_price); + let short_liquidation_price = calculate_short_bankruptcy_price(leverage_short, initial_price); wtr.write_record(["price", "payout_offer", "trader"])?; wtr.write_record(&[0.to_string(), total_collateral.to_string(), 0.to_string()])?; @@ -394,8 +394,8 @@ pub fn should_payouts_as_csv_long( let file = File::create(csv_path)?; let mut wtr = csv::WriterBuilder::new().delimiter(b';').from_writer(file); - let long_liquidation_price = calculate_long_liquidation_price(leverage_long, initial_price); - let short_liquidation_price = calculate_short_liquidation_price(leverage_short, initial_price); + let long_liquidation_price = calculate_long_bankruptcy_price(leverage_long, initial_price); + let short_liquidation_price = calculate_short_bankruptcy_price(leverage_short, initial_price); wtr.write_record(["price", "payout_offer", "trader"])?; wtr.write_record(&[ diff --git a/crates/payout_curve/src/lib.rs b/crates/payout_curve/src/lib.rs index e0955bfa8..3452c7c50 100644 --- a/crates/payout_curve/src/lib.rs +++ b/crates/payout_curve/src/lib.rs @@ -410,9 +410,9 @@ mod tests { use rust_decimal_macros::dec; use std::fs::File; use std::ops::Mul; - use trade::cfd::calculate_long_liquidation_price; + use trade::cfd::calculate_long_bankruptcy_price; use trade::cfd::calculate_margin; - use trade::cfd::calculate_short_liquidation_price; + use trade::cfd::calculate_short_bankruptcy_price; /// set this to true to export test data to csv files /// An example gnuplot file has been provided in [`payout_curve.gp`] @@ -542,11 +542,11 @@ mod tests { Amount::from_sat(calculate_margin(initial_price, quantity, short_leverage)); let collateral_reserve_offer = Amount::from_sat(300_000); - let long_liquidation_price = calculate_long_liquidation_price( + let long_liquidation_price = calculate_long_bankruptcy_price( Decimal::from_f32(long_leverage).expect("to fit into f32"), initial_price, ); - let short_liquidation_price = calculate_short_liquidation_price( + let short_liquidation_price = calculate_short_bankruptcy_price( Decimal::from_f32(short_leverage).expect("to fit into f32"), initial_price, ); @@ -681,11 +681,11 @@ mod tests { let collateral_reserve_offer = Amount::from_sat(155); - let long_liquidation_price = calculate_long_liquidation_price( + let long_liquidation_price = calculate_long_bankruptcy_price( Decimal::from_f32(long_leverage).expect("to fit into f32"), initial_price, ); - let short_liquidation_price = calculate_short_liquidation_price( + let short_liquidation_price = calculate_short_bankruptcy_price( Decimal::from_f32(short_leverage).expect("to fit into f32"), initial_price, ); @@ -986,11 +986,11 @@ mod tests { Amount::from_sat(collateral_reserve) }; - let long_liquidation_price = calculate_long_liquidation_price( + let long_liquidation_price = calculate_long_bankruptcy_price( Decimal::from_f32(long_leverage).expect("to fit into f32"), initial_price, ); - let short_liquidation_price = calculate_short_liquidation_price( + let short_liquidation_price = calculate_short_bankruptcy_price( Decimal::from_f32(short_leverage).expect("to fit into f32"), initial_price, ); diff --git a/crates/payout_curve/tests/integration_proptests.rs b/crates/payout_curve/tests/integration_proptests.rs index b86040830..9682ee744 100644 --- a/crates/payout_curve/tests/integration_proptests.rs +++ b/crates/payout_curve/tests/integration_proptests.rs @@ -20,9 +20,9 @@ use rust_decimal_macros::dec; use std::fs::File; use std::time::SystemTime; use std::time::UNIX_EPOCH; -use trade::cfd::calculate_long_liquidation_price; +use trade::cfd::calculate_long_bankruptcy_price; use trade::cfd::calculate_margin; -use trade::cfd::calculate_short_liquidation_price; +use trade::cfd::calculate_short_bankruptcy_price; use trade::Direction; /// set this to true to export test data to csv files @@ -47,11 +47,11 @@ fn calculating_payout_curve_doesnt_crash_1() { Direction::Short => (leverage_trader, leverage_coordinator), }; - let long_liquidation_price = calculate_long_liquidation_price( + let long_liquidation_price = calculate_long_bankruptcy_price( Decimal::from_f32(leverage_long).expect("to be able to parse f32"), initial_price, ); - let short_liquidation_price = calculate_short_liquidation_price( + let short_liquidation_price = calculate_short_bankruptcy_price( Decimal::from_f32(leverage_short).expect("to be able to parse f32"), initial_price, ); @@ -89,11 +89,11 @@ fn calculating_payout_curve_doesnt_crash_2() { Direction::Short => (leverage_trader, leverage_coordinator), }; - let long_liquidation_price = calculate_long_liquidation_price( + let long_liquidation_price = calculate_long_bankruptcy_price( Decimal::from_f32(leverage_long).expect("to be able to parse f32"), initial_price, ); - let short_liquidation_price = calculate_short_liquidation_price( + let short_liquidation_price = calculate_short_bankruptcy_price( Decimal::from_f32(leverage_short).expect("to be able to parse f32"), initial_price, ); @@ -131,11 +131,11 @@ fn calculating_payout_curve_doesnt_crash_3() { Direction::Short => (leverage_trader, leverage_coordinator), }; - let long_liquidation_price = calculate_long_liquidation_price( + let long_liquidation_price = calculate_long_bankruptcy_price( Decimal::from_f32(leverage_long).expect("to be able to parse f32"), initial_price, ); - let short_liquidation_price = calculate_short_liquidation_price( + let short_liquidation_price = calculate_short_bankruptcy_price( Decimal::from_f32(leverage_short).expect("to be able to parse f32"), initial_price, ); @@ -182,11 +182,11 @@ proptest! { Direction::Short => (leverage_trader, leverage_coordinator), }; - let long_liquidation_price = calculate_long_liquidation_price( + let long_liquidation_price = calculate_long_bankruptcy_price( Decimal::from_f32(leverage_long).expect("to be able to parse f32"), initial_price, ); - let short_liquidation_price = calculate_short_liquidation_price( + let short_liquidation_price = calculate_short_bankruptcy_price( Decimal::from_f32(leverage_short).expect("to be able to parse f32"), initial_price, ); diff --git a/crates/trade/src/cfd.rs b/crates/trade/src/cfd.rs index 46f6ac33f..f07ea4f99 100644 --- a/crates/trade/src/cfd.rs +++ b/crates/trade/src/cfd.rs @@ -46,18 +46,34 @@ pub fn calculate_quantity(opening_price: f32, margin: u64, leverage: f32) -> f32 quantity.to_f32().expect("quantity to fit into f32") } -pub fn calculate_long_liquidation_price(leverage: Decimal, price: Decimal) -> Decimal { - price * leverage / (leverage + Decimal::ONE) +pub fn calculate_long_bankruptcy_price(leverage: Decimal, price: Decimal) -> Decimal { + calculate_long_liquidation_price(leverage, price, Decimal::ZERO) +} + +pub fn calculate_long_liquidation_price( + leverage: Decimal, + price: Decimal, + maintenance_margin: Decimal, +) -> Decimal { + price * leverage / (leverage + Decimal::ONE - (maintenance_margin * leverage)) +} + +pub fn calculate_short_bankruptcy_price(leverage: Decimal, price: Decimal) -> Decimal { + calculate_short_liquidation_price(leverage, price, Decimal::ZERO) } /// Calculate liquidation price for the party going short. -pub fn calculate_short_liquidation_price(leverage: Decimal, price: Decimal) -> Decimal { +pub fn calculate_short_liquidation_price( + leverage: Decimal, + price: Decimal, + maintenance_margin: Decimal, +) -> Decimal { // If the leverage is equal to 1, the liquidation price will go towards infinity if leverage == Decimal::ONE { return Decimal::from(BTCUSD_MAX_PRICE); } - price * leverage / (leverage - Decimal::ONE) + price * leverage / (leverage - Decimal::ONE + (maintenance_margin * leverage)) } /// Compute the payout for the given CFD parameters at a particular `closing_price`. diff --git a/mobile/lib/common/domain/tentenone_config.dart b/mobile/lib/common/domain/tentenone_config.dart index f566a3705..114415a8b 100644 --- a/mobile/lib/common/domain/tentenone_config.dart +++ b/mobile/lib/common/domain/tentenone_config.dart @@ -14,6 +14,6 @@ class TenTenOneConfig { static bridge.TenTenOneConfig apiDummy() { return const bridge.TenTenOneConfig( - liquidityOptions: [], minQuantity: 1, marginCallPercentage: 0.1); + liquidityOptions: [], minQuantity: 1, maintenanceMargin: 0.1); } } diff --git a/mobile/lib/features/trade/trade_bottom_sheet_tab.dart b/mobile/lib/features/trade/trade_bottom_sheet_tab.dart index 014c66cc2..6f0f59fb4 100644 --- a/mobile/lib/features/trade/trade_bottom_sheet_tab.dart +++ b/mobile/lib/features/trade/trade_bottom_sheet_tab.dart @@ -308,20 +308,8 @@ class _TradeBottomSheetTabState extends State { selector: (_, provider) => provider.fromDirection(direction).liquidationPrice ?? 0.0, builder: (context, liquidationPrice, child) { - switch (direction) { - case Direction.long: - return ValueDataRow( - type: ValueType.fiat, - value: liquidationPrice + - liquidationPrice * channelTradeConstraints.marginCallPercentage, - label: "Liquidation:"); - case Direction.short: - return ValueDataRow( - type: ValueType.fiat, - value: liquidationPrice - - liquidationPrice * channelTradeConstraints.marginCallPercentage, - label: "Liquidation:"); - } + return ValueDataRow( + type: ValueType.fiat, value: liquidationPrice, label: "Liquidation:"); }), const SizedBox(width: 55), Selector( diff --git a/mobile/native/src/api.rs b/mobile/native/src/api.rs index 2563cd21f..e35abe81b 100644 --- a/mobile/native/src/api.rs +++ b/mobile/native/src/api.rs @@ -63,7 +63,7 @@ pub fn init_logging(sink: StreamSink) { pub struct TenTenOneConfig { pub liquidity_options: Vec, pub min_quantity: u64, - pub margin_call_percentage: f32, + pub maintenance_margin: f32, } impl From for TenTenOneConfig { @@ -75,7 +75,7 @@ impl From for TenTenOneConfig { .map(|lo| lo.into()) .collect(), min_quantity: value.min_quantity, - margin_call_percentage: value.margin_call_percentage, + maintenance_margin: value.maintenance_margin, } } } @@ -294,8 +294,12 @@ pub fn calculate_liquidation_price( leverage: f32, direction: Direction, ) -> SyncReturn { + let maintenance_margin = ln_dlc::get_maintenance_margin(); SyncReturn(calculations::calculate_liquidation_price( - price, leverage, direction, + price, + leverage, + direction, + maintenance_margin, )) } diff --git a/mobile/native/src/calculations/mod.rs b/mobile/native/src/calculations/mod.rs index 8de885b53..61bdd584a 100644 --- a/mobile/native/src/calculations/mod.rs +++ b/mobile/native/src/calculations/mod.rs @@ -49,7 +49,12 @@ pub fn calculate_pnl( ) } -pub fn calculate_liquidation_price(price: f32, leverage: f32, direction: Direction) -> f32 { +pub fn calculate_liquidation_price( + price: f32, + leverage: f32, + direction: Direction, + maintenance_margin: Decimal, +) -> f32 { let initial_price = Decimal::try_from(price).expect("Price to fit"); tracing::trace!("Initial price: {}", price); @@ -57,8 +62,12 @@ pub fn calculate_liquidation_price(price: f32, leverage: f32, direction: Directi let leverage = Decimal::try_from(leverage).expect("leverage to fix into decimal"); let liquidation_price = match direction { - Direction::Long => cfd::calculate_long_liquidation_price(leverage, initial_price), - Direction::Short => cfd::calculate_short_liquidation_price(leverage, initial_price), + Direction::Long => { + cfd::calculate_long_liquidation_price(leverage, initial_price, maintenance_margin) + } + Direction::Short => { + cfd::calculate_short_liquidation_price(leverage, initial_price, maintenance_margin) + } }; let liquidation_price = liquidation_price.to_f32().expect("price to fit into f32"); diff --git a/mobile/native/src/channel_trade_constraints.rs b/mobile/native/src/channel_trade_constraints.rs index d7c8f1ab3..7df1cfebf 100644 --- a/mobile/native/src/channel_trade_constraints.rs +++ b/mobile/native/src/channel_trade_constraints.rs @@ -24,9 +24,9 @@ pub struct TradeConstraints { pub is_channel_balance: bool, /// Smallest allowed margin pub min_margin: u64, - /// Margin call percentage. The percentage at which the position gets liquidated before - /// actually reaching the liquidation price. - pub margin_call_percentage: f32, + /// The maintenance margin in percent defines the margin requirement left in the dlc channel. + /// If the margin drops below that value the position gets liquidated. + pub maintenance_margin: f32, } pub fn channel_trade_constraints() -> Result { @@ -38,7 +38,7 @@ pub fn channel_trade_constraints() -> Result { let min_margin = signed_channel.map(|_| 1).unwrap_or(250_000); let min_quantity = config.min_quantity; - let margin_call_percentage = config.margin_call_percentage; + let maintenance_margin = config.maintenance_margin; // TODO(bonomat): this logic should be removed once we have our liquidity options again and the // on-boarding logic. For now we take the highest liquidity option @@ -69,7 +69,7 @@ pub fn channel_trade_constraints() -> Result { min_quantity, is_channel_balance: false, min_margin, - margin_call_percentage, + maintenance_margin, } } Some(_) => { @@ -83,7 +83,7 @@ pub fn channel_trade_constraints() -> Result { min_quantity, is_channel_balance: true, min_margin, - margin_call_percentage, + maintenance_margin, } } }; diff --git a/mobile/native/src/emergency_kit.rs b/mobile/native/src/emergency_kit.rs index 1fdf3546c..b3b8031af 100644 --- a/mobile/native/src/emergency_kit.rs +++ b/mobile/native/src/emergency_kit.rs @@ -4,6 +4,7 @@ use crate::db; use crate::db::connection; use crate::event; use crate::event::EventInternal; +use crate::get_maintenance_margin; use crate::ln_dlc; use crate::state::get_node; use crate::trade::position::Position; @@ -112,8 +113,13 @@ pub fn recreate_position() -> Result<()> { } }; - let liquidation_price = - calculate_liquidation_price(average_entry_price, order.leverage, order.direction); + let maintenance_margin = get_maintenance_margin(); + let liquidation_price = calculate_liquidation_price( + average_entry_price, + order.leverage, + order.direction, + maintenance_margin, + ); let position = Position { leverage: order.leverage, diff --git a/mobile/native/src/lib.rs b/mobile/native/src/lib.rs index 49669f8bc..b49bf1f58 100644 --- a/mobile/native/src/lib.rs +++ b/mobile/native/src/lib.rs @@ -26,6 +26,8 @@ mod orderbook; mod polls; mod storage; +pub use ln_dlc::get_maintenance_margin; + #[allow( clippy::all, clippy::unwrap_used, diff --git a/mobile/native/src/ln_dlc/mod.rs b/mobile/native/src/ln_dlc/mod.rs index a86dcef81..ffa5a2a80 100644 --- a/mobile/native/src/ln_dlc/mod.rs +++ b/mobile/native/src/ln_dlc/mod.rs @@ -72,6 +72,7 @@ use ln_dlc_node::ConfirmationStatus; use ln_dlc_storage::DlcChannelEvent; use rust_decimal::prelude::ToPrimitive; use rust_decimal::Decimal; +use rust_decimal_macros::dec; use std::net::IpAddr; use std::net::Ipv4Addr; use std::net::SocketAddr; @@ -168,6 +169,13 @@ pub fn get_seed_phrase() -> Vec { state::get_seed().get_seed_phrase() } +pub fn get_maintenance_margin() -> Decimal { + match state::try_get_tentenone_config() { + Some(config) => Decimal::try_from(config.maintenance_margin).expect("to fit into decimal"), + None => dec!(0.1), + } +} + /// Gets the seed from the storage or from disk. However it will panic if the seed can not be found. /// No new seed will be created. fn get_seed() -> Bip39Seed { diff --git a/mobile/native/src/trade/position/mod.rs b/mobile/native/src/trade/position/mod.rs index eed6629ef..2722c6a5e 100644 --- a/mobile/native/src/trade/position/mod.rs +++ b/mobile/native/src/trade/position/mod.rs @@ -1,5 +1,6 @@ use crate::calculations::calculate_liquidation_price; use crate::calculations::calculate_pnl; +use crate::get_maintenance_margin; use crate::trade::order::Order; use crate::trade::order::OrderState; use crate::trade::order::OrderType; @@ -88,8 +89,13 @@ impl Position { let average_entry_price = order.execution_price().expect("order to be filled"); - let liquidation_price = - calculate_liquidation_price(average_entry_price, order.leverage, order.direction); + let maintenance_margin = get_maintenance_margin(); + let liquidation_price = calculate_liquidation_price( + average_entry_price, + order.leverage, + order.direction, + maintenance_margin, + ); let contracts = decimal_from_f32(order.quantity); @@ -493,10 +499,12 @@ impl Position { / (starting_contracts_relative / starting_average_execution_price + order_contracts_relative / order_execution_price); + let maintenance_margin = get_maintenance_margin(); let updated_liquidation_price = calculate_liquidation_price( f32_from_decimal(updated_average_execution_price), f32_from_decimal(starting_leverage), self.direction, + maintenance_margin, ); let updated_collateral = { @@ -775,7 +783,7 @@ mod tests { assert_eq!(updated_position.contract_symbol, position.contract_symbol); assert_eq!(updated_position.direction, position.direction); assert_eq!(updated_position.average_entry_price, 36_446.805); - assert_eq!(updated_position.liquidation_price, 24297.873); + assert_eq!(updated_position.liquidation_price, 26033.436); assert_eq!(updated_position.position_state, PositionState::Open); assert_eq!(updated_position.collateral, 20_578); assert!(!updated_position.stable); @@ -923,7 +931,7 @@ mod tests { assert_eq!(updated_position.contract_symbol, position.contract_symbol); assert_eq!(updated_position.direction, order.direction); assert_eq!(updated_position.average_entry_price, 36_401.5); - assert_eq!(updated_position.liquidation_price, 24_267.666); + assert_eq!(updated_position.liquidation_price, 26001.072); assert_eq!(updated_position.position_state, PositionState::Open); assert_eq!(updated_position.collateral, 13_736); assert!(!updated_position.stable); diff --git a/mobile/test/trade_test.dart b/mobile/test/trade_test.dart index d199af736..acce404f3 100644 --- a/mobile/test/trade_test.dart +++ b/mobile/test/trade_test.dart @@ -123,7 +123,7 @@ void main() { minQuantity: 1, isChannelBalance: true, minMargin: 1, - marginCallPercentage: 0.1)); + maintenanceMargin: 0.1)); when(candlestickService.fetchCandles(1000)).thenAnswer((_) async { return getDummyCandles(1000); @@ -239,7 +239,7 @@ void main() { minQuantity: 1, isChannelBalance: true, minMargin: 1, - marginCallPercentage: 0.1)); + maintenanceMargin: 0.1)); when(dlcChannelService.getEstimatedChannelFeeReserve()).thenReturn((Amount(500)));