Browse Source

Merge #433

433: Rename Buy and Sell to Long and Short r=da-kami a=DeliciousHair

Addresses #373. Change made for consistency of language and removes some ambiguity in the business logic as a side-effect.

Co-authored-by: DelicioiusHair <mshepit@gmail.com>
burn-down-handle
bors[bot] 3 years ago
committed by GitHub
parent
commit
1d16258a52
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      daemon/src/model.rs
  2. 125
      daemon/src/model/cfd.rs
  3. 4
      daemon/src/routes_taker.rs
  4. 2
      docs/asset/mvp_sequence_diagram.puml
  5. 10
      docs/roadmap.md
  6. 2
      frontend/src/TakerApp.tsx
  7. 8
      frontend/src/components/Types.tsx

4
daemon/src/model.rs

@ -441,8 +441,8 @@ pub enum TradingPair {
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, sqlx::Type)]
pub enum Position {
Buy,
Sell,
Long,
Short,
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]

125
daemon/src/model/cfd.rs

@ -136,7 +136,7 @@ impl Order {
settlement_time_interval_hours: Duration,
) -> Result<Self> {
let leverage = Leverage::new(2)?;
let liquidation_price = calculate_liquidation_price(leverage, price);
let liquidation_price = calculate_long_liquidation_price(leverage, price);
Ok(Order {
id: OrderId::default(),
@ -146,7 +146,7 @@ impl Order {
leverage,
trading_pair: TradingPair::BtcUsd,
liquidation_price,
position: Position::Sell,
position: Position::Short,
creation_timestamp: SystemTime::now(),
settlement_time_interval_hours,
origin,
@ -589,10 +589,10 @@ impl Cfd {
pub fn margin(&self) -> Result<Amount> {
let margin = match self.position() {
Position::Buy => {
calculate_buy_margin(self.order.price, self.quantity_usd, self.order.leverage)
Position::Long => {
calculate_long_margin(self.order.price, self.quantity_usd, self.order.leverage)
}
Position::Sell => calculate_sell_margin(self.order.price, self.quantity_usd),
Position::Short => calculate_short_margin(self.order.price, self.quantity_usd),
};
Ok(margin)
@ -600,9 +600,9 @@ impl Cfd {
pub fn counterparty_margin(&self) -> Result<Amount> {
let margin = match self.position() {
Position::Buy => calculate_sell_margin(self.order.price, self.quantity_usd),
Position::Sell => {
calculate_buy_margin(self.order.price, self.quantity_usd, self.order.leverage)
Position::Long => calculate_short_margin(self.order.price, self.quantity_usd),
Position::Short => {
calculate_long_margin(self.order.price, self.quantity_usd, self.order.leverage)
}
};
@ -657,8 +657,8 @@ impl Cfd {
// If the order is not our own we take the counter-position in the CFD
Origin::Theirs => match self.order.position {
Position::Buy => Position::Sell,
Position::Sell => Position::Buy,
Position::Long => Position::Short,
Position::Short => Position::Long,
},
}
}
@ -1301,28 +1301,33 @@ impl AsBlocks for Duration {
}
}
/// Calculates the buyer's margin in BTC
/// Calculates the long's margin in BTC
///
/// The margin is the initial margin and represents the collateral the buyer has to come up with to
/// satisfy the contract. Here we calculate the initial buy margin as: quantity / (initial_price *
/// leverage)
pub fn calculate_buy_margin(price: Price, quantity: Usd, leverage: Leverage) -> Amount {
/// The margin is the initial margin and represents the collateral the buyer
/// has to come up with to satisfy the contract. Here we calculate the initial
/// long margin as: quantity / (initial_price * leverage)
pub fn calculate_long_margin(price: Price, quantity: Usd, leverage: Leverage) -> Amount {
quantity / (price * leverage)
}
/// Calculates the seller's margin in BTC
/// Calculates the shorts's margin in BTC
///
/// The seller margin is represented as the quantity of the contract given the initial price.
/// The seller can currently not leverage the position but always has to cover the complete
/// quantity.
fn calculate_sell_margin(price: Price, quantity: Usd) -> Amount {
/// The short margin is represented as the quantity of the contract given the
/// initial price. The short side can currently not leverage the position but
/// always has to cover the complete quantity.
fn calculate_short_margin(price: Price, quantity: Usd) -> Amount {
quantity / price
}
fn calculate_liquidation_price(leverage: Leverage, price: Price) -> Price {
fn calculate_long_liquidation_price(leverage: Leverage, price: Price) -> Price {
price * leverage / (leverage + 1)
}
// PLACEHOLDER
// fn calculate_short_liquidation_price(leverage: Leverage, price: Price) -> Price {
// price * leverage / (leverage - 1)
// }
/// Returns the Profit/Loss (P/L) as Bitcoin. Losses are capped by the provided margin
fn calculate_profit(
initial_price: Price,
@ -1335,13 +1340,13 @@ fn calculate_profit(
InversePrice::new(initial_price).context("cannot invert invalid price")?;
let inv_closing_price =
InversePrice::new(closing_price).context("cannot invert invalid price")?;
let long_liquidation_price = calculate_liquidation_price(leverage, initial_price);
let long_liquidation_price = calculate_long_liquidation_price(leverage, initial_price);
let long_is_liquidated = closing_price <= long_liquidation_price;
let long_margin = calculate_buy_margin(initial_price, quantity, leverage)
let long_margin = calculate_long_margin(initial_price, quantity, leverage)
.to_signed()
.context("Unable to compute long margin")?;
let short_margin = calculate_sell_margin(initial_price, quantity)
let short_margin = calculate_short_margin(initial_price, quantity)
.to_signed()
.context("Unable to compute short margin")?;
let amount_changed = (quantity * inv_initial_price)
@ -1354,12 +1359,9 @@ fn calculate_profit(
// calculate profit/loss (P and L) in BTC
let (margin, payout) = match position {
// TODO:
// Assuming that Buy == Taker, Sell == Maker which in turn has
// implications for being short or long (since taker can only go
// long at the momnet) and if leverage can be used
// (long_leverage == leverage, short_leverage == 1) which also
// has the effect that the right boundary `b` below is infinite
// and not used.
// At this point, long_leverage == leverage, short_leverage == 1
// which has the effect that the right boundary `b` below is
// infinite and not used.
//
// The general case is:
// let:
@ -1384,14 +1386,14 @@ fn calculate_profit(
// Q / (xi * Ls) - Q * (1 / xi - 1 / xc) if a < xc < b,
// 0 if xc >= b
// }
Position::Buy => {
Position::Long => {
let payout = match long_is_liquidated {
true => SignedAmount::ZERO,
false => long_margin + amount_changed,
};
(long_margin, payout)
}
Position::Sell => {
Position::Short => {
let payout = match long_is_liquidated {
true => long_margin + short_margin,
false => short_margin - amount_changed,
@ -1418,61 +1420,61 @@ mod tests {
let leverage = Leverage::new(5).unwrap();
let expected = Price::new(dec!(38437.5)).unwrap();
let liquidation_price = calculate_liquidation_price(leverage, price);
let liquidation_price = calculate_long_liquidation_price(leverage, price);
assert_eq!(liquidation_price, expected);
}
#[test]
fn given_leverage_of_one_and_equal_price_and_quantity_then_buy_margin_is_one_btc() {
fn given_leverage_of_one_and_equal_price_and_quantity_then_long_margin_is_one_btc() {
let price = Price::new(dec!(40000)).unwrap();
let quantity = Usd::new(dec!(40000));
let leverage = Leverage::new(1).unwrap();
let buy_margin = calculate_buy_margin(price, quantity, leverage);
let long_margin = calculate_long_margin(price, quantity, leverage);
assert_eq!(buy_margin, Amount::ONE_BTC);
assert_eq!(long_margin, Amount::ONE_BTC);
}
#[test]
fn given_leverage_of_one_and_leverage_of_ten_then_buy_margin_is_lower_factor_ten() {
fn given_leverage_of_one_and_leverage_of_ten_then_long_margin_is_lower_factor_ten() {
let price = Price::new(dec!(40000)).unwrap();
let quantity = Usd::new(dec!(40000));
let leverage = Leverage::new(10).unwrap();
let buy_margin = calculate_buy_margin(price, quantity, leverage);
let long_margin = calculate_long_margin(price, quantity, leverage);
assert_eq!(buy_margin, Amount::from_btc(0.1).unwrap());
assert_eq!(long_margin, Amount::from_btc(0.1).unwrap());
}
#[test]
fn given_quantity_equals_price_then_sell_margin_is_one_btc() {
fn given_quantity_equals_price_then_short_margin_is_one_btc() {
let price = Price::new(dec!(40000)).unwrap();
let quantity = Usd::new(dec!(40000));
let sell_margin = calculate_sell_margin(price, quantity);
let short_margin = calculate_short_margin(price, quantity);
assert_eq!(sell_margin, Amount::ONE_BTC);
assert_eq!(short_margin, Amount::ONE_BTC);
}
#[test]
fn given_quantity_half_of_price_then_sell_margin_is_half_btc() {
fn given_quantity_half_of_price_then_short_margin_is_half_btc() {
let price = Price::new(dec!(40000)).unwrap();
let quantity = Usd::new(dec!(20000));
let sell_margin = calculate_sell_margin(price, quantity);
let short_margin = calculate_short_margin(price, quantity);
assert_eq!(sell_margin, Amount::from_btc(0.5).unwrap());
assert_eq!(short_margin, Amount::from_btc(0.5).unwrap());
}
#[test]
fn given_quantity_double_of_price_then_sell_margin_is_two_btc() {
fn given_quantity_double_of_price_then_short_margin_is_two_btc() {
let price = Price::new(dec!(40000)).unwrap();
let quantity = Usd::new(dec!(80000));
let sell_margin = calculate_sell_margin(price, quantity);
let short_margin = calculate_short_margin(price, quantity);
assert_eq!(sell_margin, Amount::from_btc(2.0).unwrap());
assert_eq!(short_margin, Amount::from_btc(2.0).unwrap());
}
#[test]
@ -1499,7 +1501,7 @@ mod tests {
Price::new(dec!(10_000)).unwrap(),
Usd::new(dec!(10_000)),
Leverage::new(2).unwrap(),
Position::Buy,
Position::Long,
SignedAmount::ZERO,
Decimal::ZERO.into(),
"No price increase means no profit",
@ -1510,7 +1512,7 @@ mod tests {
Price::new(dec!(20_000)).unwrap(),
Usd::new(dec!(10_000)),
Leverage::new(2).unwrap(),
Position::Buy,
Position::Long,
SignedAmount::from_sat(50_000_000),
dec!(100).into(),
"A price increase of 2x should result in a profit of 100% (long)",
@ -1521,7 +1523,7 @@ mod tests {
Price::new(dec!(6_000)).unwrap(),
Usd::new(dec!(9_000)),
Leverage::new(2).unwrap(),
Position::Buy,
Position::Long,
SignedAmount::from_sat(-50_000_000),
dec!(-100).into(),
"A price drop of 1/(Leverage + 1) x should result in 100% loss (long)",
@ -1532,7 +1534,7 @@ mod tests {
Price::new(dec!(5_000)).unwrap(),
Usd::new(dec!(10_000)),
Leverage::new(2).unwrap(),
Position::Buy,
Position::Long,
SignedAmount::from_sat(-50_000_000),
dec!(-100).into(),
"A loss should be capped at 100% (long)",
@ -1543,7 +1545,7 @@ mod tests {
Price::new(dec!(60_000)).unwrap(),
Usd::new(dec!(10_000)),
Leverage::new(2).unwrap(),
Position::Buy,
Position::Long,
SignedAmount::from_sat(3_174_603),
dec!(31.99999798400001).into(),
"long position should make a profit when price goes up",
@ -1554,10 +1556,10 @@ mod tests {
Price::new(dec!(60_000)).unwrap(),
Usd::new(dec!(10_000)),
Leverage::new(2).unwrap(),
Position::Sell,
Position::Short,
SignedAmount::from_sat(-3_174_603),
dec!(-15.99999899200001).into(),
"sell position should make a loss when price goes up",
"short position should make a loss when price goes up",
);
}
@ -1590,7 +1592,7 @@ mod tests {
closing_price,
quantity,
leverage,
Position::Buy,
Position::Long,
)
.unwrap();
let (loss, loss_in_percent) = calculate_profit(
@ -1598,7 +1600,7 @@ mod tests {
closing_price,
quantity,
leverage,
Position::Sell,
Position::Short,
)
.unwrap();
@ -1616,10 +1618,10 @@ mod tests {
let initial_price = Price::new(dec!(15_000)).unwrap();
let quantity = Usd::new(dec!(10_000));
let leverage = Leverage::new(2).unwrap();
let long_margin = calculate_buy_margin(initial_price, quantity, leverage)
let long_margin = calculate_long_margin(initial_price, quantity, leverage)
.to_signed()
.unwrap();
let short_margin = calculate_sell_margin(initial_price, quantity)
let short_margin = calculate_short_margin(initial_price, quantity)
.to_signed()
.unwrap();
let pool_amount = SignedAmount::ONE_BTC;
@ -1637,9 +1639,10 @@ mod tests {
for price in closing_prices {
let (long_profit, _) =
calculate_profit(initial_price, price, quantity, leverage, Position::Buy).unwrap();
calculate_profit(initial_price, price, quantity, leverage, Position::Long).unwrap();
let (short_profit, _) =
calculate_profit(initial_price, price, quantity, leverage, Position::Sell).unwrap();
calculate_profit(initial_price, price, quantity, leverage, Position::Short)
.unwrap();
assert_eq!(
long_profit + long_margin + short_profit + short_margin,

4
daemon/src/routes_taker.rs

@ -1,5 +1,5 @@
use bdk::bitcoin::{Amount, Network};
use daemon::model::cfd::{calculate_buy_margin, Cfd, Order, OrderId, Role, UpdateCfdProposals};
use daemon::model::cfd::{calculate_long_margin, Cfd, Order, OrderId, Role, UpdateCfdProposals};
use daemon::model::{Leverage, Price, Usd, WalletInfo};
use daemon::routes::EmbeddedFileExt;
use daemon::to_sse_event::{CfdAction, CfdsWithAuxData, ToSseEvent};
@ -178,7 +178,7 @@ pub struct MarginResponse {
pub fn margin_calc(
margin_request: Json<MarginRequest>,
) -> Result<status::Accepted<Json<MarginResponse>>, status::BadRequest<String>> {
let margin = calculate_buy_margin(
let margin = calculate_long_margin(
margin_request.price,
margin_request.quantity,
margin_request.leverage,

2
docs/asset/mvp_sequence_diagram.puml

@ -34,7 +34,7 @@ UserApp -> User: Buy position open
end group
Seller -> SellerApp: Republish new sell-order
group DLC settlement
User -> UserApp: Close buy position
User -> UserApp: Close long position
UserApp -> Oracle: request attestation
Oracle --> UserApp: attested price
UserApp -> Bitcoin: CET according to price

10
docs/roadmap.md

@ -14,14 +14,14 @@ The Minimal Viable Product's goal is to showcase that non-custidial CFD trading
### In scope
For the MVP there is only one Maker that takes the selling side and creates sell orders.
For the MVP there is only one Maker that takes the short side and creates orders.
The maker does not do any automation.
The maker dictates the price.
A user is always in the role of a taker.
The user has a simple user interface and can take the maker's order there.
The taker can specify a quantity, the leverage is fixed to `x5`.
For the MVP the leverage is fixed to `x5` for both sell and buy orders.
The taker can specify a quantity, the leverage is fixed to `x2`.
For the MVP the leverage is fixed to `x1` for the maker.
The oracle is needed for attestation of prices at a certain point in time.
The oracle is to be run by a separate party that is neither the taker nor the maker.
@ -38,11 +38,11 @@ Constraints:
- Software Setup
- Taker
- Local running daemon that exposes API + web-interface for UI
- Can take a sell order (represents the buy side)
- Can take an order (represents the long side)
- Specify quantity
- (fixed leverage of `x5`)
- Maker
- Can create a sell order (represents the sell side)
- Can create an order (represents the short side)
- Sell order publication is done manually
- Take requests are accepted manually
- ♻️ Oracle

2
frontend/src/TakerApp.tsx

@ -206,7 +206,7 @@ export default function App() {
makeNewOrderRequest(payload);
}}
>
BUY
BUY LONG
</Button>
</GridItem>
</Grid>

8
frontend/src/components/Types.tsx

@ -16,17 +16,17 @@ export class Position {
public getColorScheme(): string {
switch (this.key) {
case PositionKey.BUY:
case PositionKey.LONG:
return "green";
case PositionKey.SELL:
case PositionKey.SHORT:
return "blue";
}
}
}
enum PositionKey {
BUY = "Buy",
SELL = "Sell",
LONG = "Long",
SHORT = "Short",
}
export interface Cfd {

Loading…
Cancel
Save