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

4
daemon/src/routes_taker.rs

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

2
docs/asset/mvp_sequence_diagram.puml

@ -34,7 +34,7 @@ UserApp -> User: Buy position open
end group end group
Seller -> SellerApp: Republish new sell-order Seller -> SellerApp: Republish new sell-order
group DLC settlement group DLC settlement
User -> UserApp: Close buy position User -> UserApp: Close long position
UserApp -> Oracle: request attestation UserApp -> Oracle: request attestation
Oracle --> UserApp: attested price Oracle --> UserApp: attested price
UserApp -> Bitcoin: CET according to 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 ### 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 does not do any automation.
The maker dictates the price. The maker dictates the price.
A user is always in the role of a taker. 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 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`. The taker can specify a quantity, the leverage is fixed to `x2`.
For the MVP the leverage is fixed to `x5` for both sell and buy orders. 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 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. 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 - Software Setup
- Taker - Taker
- Local running daemon that exposes API + web-interface for UI - 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 - Specify quantity
- (fixed leverage of `x5`) - (fixed leverage of `x5`)
- Maker - Maker
- Can create a sell order (represents the sell side) - Can create an order (represents the short side)
- Sell order publication is done manually - Sell order publication is done manually
- Take requests are accepted manually - Take requests are accepted manually
- ♻️ Oracle - ♻️ Oracle

2
frontend/src/TakerApp.tsx

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

8
frontend/src/components/Types.tsx

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

Loading…
Cancel
Save