use std::num::NonZeroU64;
use super::super::{
SATS_PER_BTC,
error::TradeValidationError,
leverage::Leverage,
margin::Margin,
price::{PercentageCapped, Price},
quantity::Quantity,
trade::{TradeSide, TradeSize},
};
pub fn estimate_liquidation_price(
side: TradeSide,
quantity: Quantity,
entry_price: Price,
leverage: Leverage,
) -> Price {
let quantity = quantity.as_f64();
let price = entry_price.as_f64();
let leverage = leverage.as_f64();
let a = 1.0 / price;
let floored_margin = (quantity * SATS_PER_BTC / price / leverage).floor();
let b = floored_margin / SATS_PER_BTC / quantity;
let liquidation_calc = match side {
TradeSide::Buy => 1.0 / (a + b),
TradeSide::Sell => 1.0 / (a - b).max(0.),
};
Price::bounded(liquidation_calc)
}
pub fn evaluate_open_trade_params(
side: TradeSide,
size: TradeSize,
leverage: Leverage,
entry_price: Price,
stoploss: Option<Price>,
takeprofit: Option<Price>,
fee_perc: PercentageCapped,
) -> Result<(Quantity, Margin, Price, u64, u64), TradeValidationError> {
let (quantity, margin) = size
.to_quantity_and_margin(entry_price, leverage)
.map_err(TradeValidationError::TradeParamsInvalidQuantity)?;
let liquidation = estimate_liquidation_price(side, quantity, entry_price, leverage);
match side {
TradeSide::Buy => {
if let Some(stoploss) = stoploss {
if stoploss < liquidation {
return Err(TradeValidationError::StoplossBelowLiquidationLong {
stoploss,
liquidation,
});
}
if stoploss >= entry_price {
return Err(TradeValidationError::StoplossAboveEntryForLong {
stoploss,
entry_price,
});
}
}
if let Some(takeprofit) = takeprofit
&& takeprofit <= entry_price
{
return Err(TradeValidationError::TakeprofitBelowEntryForLong {
takeprofit,
entry_price,
});
}
}
TradeSide::Sell => {
if let Some(stoploss) = stoploss {
if stoploss > liquidation {
return Err(TradeValidationError::StoplossAboveLiquidationShort {
stoploss,
liquidation,
});
}
if stoploss <= entry_price {
return Err(TradeValidationError::StoplossBelowEntryForShort {
stoploss,
entry_price,
});
}
}
if let Some(takeprofit) = takeprofit
&& takeprofit >= entry_price
{
return Err(TradeValidationError::TakeprofitAboveEntryForShort {
takeprofit,
entry_price,
});
}
}
};
let fee_calc = SATS_PER_BTC * fee_perc.as_f64() / 100.;
let opening_fee = (fee_calc * quantity.as_f64() / entry_price.as_f64()).floor() as u64;
let closing_fee_reserved = (fee_calc * quantity.as_f64() / liquidation.as_f64()).floor() as u64;
Ok((
quantity,
margin,
liquidation,
opening_fee,
closing_fee_reserved,
))
}
pub fn estimate_pl(
side: TradeSide,
quantity: Quantity,
start_price: Price,
end_price: Price,
) -> f64 {
let start_price = start_price.as_f64();
let end_price = end_price.as_f64();
let inverse_price_delta = match side {
TradeSide::Buy => SATS_PER_BTC / start_price - SATS_PER_BTC / end_price,
TradeSide::Sell => SATS_PER_BTC / end_price - SATS_PER_BTC / start_price,
};
quantity.as_f64() * inverse_price_delta
}
pub fn estimate_price_from_pl(
side: TradeSide,
quantity: Quantity,
start_price: Price,
pl: f64,
) -> Price {
let start_price = start_price.as_f64();
let quantity = quantity.as_f64();
let inverse_price_delta = pl / quantity;
let inverse_end_price = match side {
TradeSide::Buy => (SATS_PER_BTC / start_price) - inverse_price_delta,
TradeSide::Sell => (SATS_PER_BTC / start_price) + inverse_price_delta,
};
Price::bounded(SATS_PER_BTC / inverse_end_price)
}
pub fn evaluate_new_stoploss(
side: TradeSide,
liquidation: Price,
takeprofit: Option<Price>,
market_price: Price,
new_stoploss: Price,
) -> Result<(), TradeValidationError> {
match side {
TradeSide::Buy => {
if new_stoploss < liquidation {
return Err(TradeValidationError::StoplossBelowLiquidationLong {
stoploss: new_stoploss,
liquidation,
});
}
if new_stoploss >= market_price {
return Err(TradeValidationError::NewStoplossNotBelowMarketForLong {
new_stoploss,
market_price,
});
}
if let Some(takeprofit) = takeprofit
&& new_stoploss >= takeprofit
{
return Err(TradeValidationError::NewStoplossNotBelowTakeprofitForLong {
new_stoploss,
takeprofit,
});
}
}
TradeSide::Sell => {
if new_stoploss > liquidation {
return Err(TradeValidationError::StoplossAboveLiquidationShort {
stoploss: new_stoploss,
liquidation,
});
}
if new_stoploss <= market_price {
return Err(TradeValidationError::NewStoplossNotAboveMarketForShort {
new_stoploss,
market_price,
});
}
if let Some(takeprofit) = takeprofit
&& new_stoploss <= takeprofit
{
return Err(
TradeValidationError::NewStoplossNotAboveTakeprofitForShort {
new_stoploss,
takeprofit,
},
);
}
}
}
Ok(())
}
pub fn evaluate_added_margin(
side: TradeSide,
quantity: Quantity,
price: Price,
current_margin: Margin,
amount: NonZeroU64,
) -> Result<(Margin, Leverage, Price), TradeValidationError> {
let new_margin = current_margin + amount.into();
let new_leverage = Leverage::try_calculate(quantity, new_margin, price)
.map_err(TradeValidationError::AddedMarginInvalidLeverage)?;
let new_liquidation = estimate_liquidation_price(side, quantity, price, new_leverage);
Ok((new_margin, new_leverage, new_liquidation))
}
pub fn evaluate_cash_in(
side: TradeSide,
quantity: Quantity,
margin: Margin,
price: Price,
stoploss: Option<Price>,
market_price: Price,
amount: NonZeroU64,
) -> Result<(Price, Margin, Leverage, Price, Option<Price>), TradeValidationError> {
let amount = amount.get();
let current_pl = estimate_pl(side, quantity, price, market_price);
let (new_price, remaining_amount) = if current_pl > 0. {
if amount < current_pl as u64 {
let new_price = estimate_price_from_pl(side, quantity, price, amount as f64);
(new_price, 0)
} else {
(market_price, amount - current_pl as u64)
}
} else {
(price, amount)
};
let new_margin = if remaining_amount == 0 {
margin
} else {
Margin::try_from(margin.as_u64().saturating_sub(remaining_amount))
.map_err(TradeValidationError::CashInInvalidMargin)?
};
let new_leverage = Leverage::try_calculate(quantity, new_margin, new_price)
.map_err(TradeValidationError::CashInInvalidLeverage)?;
let new_liquidation = estimate_liquidation_price(side, quantity, new_price, new_leverage);
let new_stoploss = stoploss.and_then(|sl| {
let valid = match side {
TradeSide::Buy => new_liquidation <= sl,
TradeSide::Sell => new_liquidation >= sl,
};
if valid { Some(sl) } else { None }
});
Ok((
new_price,
new_margin,
new_leverage,
new_liquidation,
new_stoploss,
))
}
pub fn evaluate_collateral_delta_for_liquidation(
side: TradeSide,
quantity: Quantity,
margin: Margin,
price: Price,
liquidation: Price,
target_liquidation: Price,
market_price: Price,
) -> Result<i64, TradeValidationError> {
if target_liquidation == liquidation {
return Ok(0);
}
let target_collateral =
Margin::est_from_liquidation_price(side, quantity, market_price, target_liquidation)?;
let pl = estimate_pl(side, quantity, price, market_price);
let colateral_diff = target_collateral.as_i64() - margin.as_i64() - pl.round() as i64;
Ok(colateral_diff)
}
pub fn evaluate_closing_fee(
fee_perc: PercentageCapped,
quantity: Quantity,
close_price: Price,
) -> u64 {
let fee_calc = SATS_PER_BTC * fee_perc.as_f64() / 100.;
(fee_calc * quantity.as_f64() / close_price.as_f64()).floor() as u64
}
#[cfg(test)]
mod tests;