Browse Source

Margin calculation

The buy margin is calculated from `initial_price`, `quantity` and `leverage`.
The sell margin is calculated from `initial_price` and `quantity` (no leverage trading for the seller at the moment).

Includes several refactors:
- Separate API interface for `Cfd` and `CfdOffer` when mapping `toSseEvent`. This allows internal modelling to be different than the exposed API.
  - Remove the profit calculation internally, it is only relevant on the UI feed API level.
  - Move code that is specific to taker/maker http API from `model` to the respective `routes` module.
- Some simplification of the calculation model of `Usd` (could be further improved by deriving calculation traits)
- Introduces `OfferOrigin` used to save the offer's origin as `mine` and `others` in the database. This is used to properly derive the CFD position from the offer.
verify-transactions
Daniel Karzel 4 years ago
parent
commit
cfae4e573b
No known key found for this signature in database GPG Key ID: 30C3FC2E438ADB6E
  1. 9
      daemon/migrations/20210903050345_create_cfd_and_offer_tables.sql
  2. 42
      daemon/sqlx-data.json
  3. 61
      daemon/src/db.rs
  4. 33
      daemon/src/maker_cfd_actor.rs
  5. 47
      daemon/src/model.rs
  6. 221
      daemon/src/model/cfd.rs
  7. 15
      daemon/src/routes_maker.rs
  8. 41
      daemon/src/routes_taker.rs
  9. 3
      daemon/src/taker.rs
  10. 31
      daemon/src/taker_cfd_actor.rs
  11. 106
      daemon/src/to_sse_event.rs
  12. 63
      frontend/src/Taker.tsx
  13. 14
      frontend/src/components/CfdTile.tsx
  14. 34
      frontend/src/components/Types.tsx

9
daemon/migrations/20210903050345_create_cfd_and_offer_tables.sql

@ -11,7 +11,8 @@ create table if not exists offers
leverage integer not null, leverage integer not null,
liquidation_price text not null, liquidation_price text not null,
creation_timestamp text not null, creation_timestamp text not null,
term text not null term text not null,
origin text not null
); );
create unique index if not exists offers_uuid create unique index if not exists offers_uuid
@ -32,8 +33,8 @@ create unique index if not exists cfd_offer_uuid
create table if not exists cfd_states create table if not exists cfd_states
( (
id integer primary key autoincrement, id integer primary key autoincrement,
cfd_id integer not null, cfd_id integer not null,
state text not null, state text not null,
foreign key (cfd_id) references cfds (id) foreign key (cfd_id) references cfds (id)
); );

42
daemon/sqlx-data.json

@ -18,18 +18,8 @@
] ]
} }
}, },
"29bc1b2bd17146eb36e2c61acc1bed3c9b5b3014e35874c752cbd50016e99b74": { "2bfe23378b852e9c98f1db3e7c7694e1a7037183eb6fa8d81916cdb46c760547": {
"query": "\n insert into offers (\n uuid,\n trading_pair,\n position,\n initial_price,\n min_quantity,\n max_quantity,\n leverage,\n liquidation_price,\n creation_timestamp,\n term\n ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);\n ", "query": "\n select\n cfds.id as cfd_id,\n offers.uuid as offer_id,\n offers.initial_price as initial_price,\n offers.leverage as leverage,\n offers.trading_pair as trading_pair,\n offers.position as position,\n offers.origin as origin,\n offers.liquidation_price as liquidation_price,\n cfds.quantity_usd as quantity_usd,\n cfd_states.state as state\n from cfds as cfds\n inner join offers as offers on cfds.offer_id = offers.id\n inner join cfd_states as cfd_states on cfd_states.cfd_id = cfds.id\n where cfd_states.state in (\n select\n state\n from cfd_states\n where cfd_id = cfds.id\n order by id desc\n limit 1\n )\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 10
},
"nullable": []
}
},
"4896e2d2e2c6cc03f9ae2b7de85279f295fb7e70e083e0a1a3faf3e7551650f3": {
"query": "\n select\n cfds.id as cfd_id,\n offers.uuid as offer_id,\n offers.initial_price as initial_price,\n offers.leverage as leverage,\n offers.trading_pair as trading_pair,\n offers.position as position,\n offers.liquidation_price as liquidation_price,\n cfds.quantity_usd as quantity_usd,\n cfd_states.state as state\n from cfds as cfds\n inner join offers as offers on cfds.offer_id = offers.id\n inner join cfd_states as cfd_states on cfd_states.cfd_id = cfds.id\n where cfd_states.state in (\n select\n state\n from cfd_states\n where cfd_id = cfds.id\n order by id desc\n limit 1\n )\n ",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@ -63,19 +53,24 @@
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "liquidation_price", "name": "origin",
"ordinal": 6, "ordinal": 6,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "quantity_usd", "name": "liquidation_price",
"ordinal": 7, "ordinal": 7,
"type_info": "Text" "type_info": "Text"
}, },
{ {
"name": "state", "name": "quantity_usd",
"ordinal": 8, "ordinal": 8,
"type_info": "Text" "type_info": "Text"
},
{
"name": "state",
"ordinal": 9,
"type_info": "Text"
} }
], ],
"parameters": { "parameters": {
@ -90,10 +85,21 @@
false, false,
false, false,
false, false,
false,
false false
] ]
} }
}, },
"4a8db91aaef56a804d0151460297175355c9adee100b87fa9052245cdd18d7e9": {
"query": "\n insert into offers (\n uuid,\n trading_pair,\n position,\n initial_price,\n min_quantity,\n max_quantity,\n leverage,\n liquidation_price,\n creation_timestamp,\n term,\n origin\n ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 11
},
"nullable": []
}
},
"50abbb297394739ec9d85917f8c32aa8bcfa0bfe140b24e9eeda4ce8d30d4f8d": { "50abbb297394739ec9d85917f8c32aa8bcfa0bfe140b24e9eeda4ce8d30d4f8d": {
"query": "\n select\n state\n from cfd_states\n where cfd_id = ?\n order by id desc\n limit 1;\n ", "query": "\n select\n state\n from cfd_states\n where cfd_id = ?\n order by id desc\n limit 1;\n ",
"describe": { "describe": {
@ -190,6 +196,11 @@
"name": "term", "name": "term",
"ordinal": 10, "ordinal": 10,
"type_info": "Text" "type_info": "Text"
},
{
"name": "origin",
"ordinal": 11,
"type_info": "Text"
} }
], ],
"parameters": { "parameters": {
@ -206,6 +217,7 @@
false, false,
false, false,
false, false,
false,
false false
] ]
} }

61
daemon/src/db.rs

@ -1,13 +1,19 @@
use crate::model::cfd::{Cfd, CfdOffer, CfdOfferId, CfdState}; use crate::model::cfd::{Cfd, CfdOffer, CfdOfferId, CfdState};
use crate::model::{Leverage, Usd}; use crate::model::{Leverage, Position};
use anyhow::Context; use anyhow::Context;
use bdk::bitcoin::Amount;
use rocket_db_pools::sqlx; use rocket_db_pools::sqlx;
use serde::{Deserialize, Serialize};
use sqlx::pool::PoolConnection; use sqlx::pool::PoolConnection;
use sqlx::{Acquire, Sqlite, SqlitePool}; use sqlx::{Acquire, Sqlite, SqlitePool};
use std::convert::TryInto; use std::convert::TryInto;
use std::mem; use std::mem;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum OfferOrigin {
Mine,
Others,
}
pub async fn run_migrations(pool: &SqlitePool) -> anyhow::Result<()> { pub async fn run_migrations(pool: &SqlitePool) -> anyhow::Result<()> {
sqlx::migrate!("./migrations").run(pool).await?; sqlx::migrate!("./migrations").run(pool).await?;
Ok(()) Ok(())
@ -16,6 +22,7 @@ pub async fn run_migrations(pool: &SqlitePool) -> anyhow::Result<()> {
pub async fn insert_cfd_offer( pub async fn insert_cfd_offer(
cfd_offer: &CfdOffer, cfd_offer: &CfdOffer,
conn: &mut PoolConnection<Sqlite>, conn: &mut PoolConnection<Sqlite>,
origin: OfferOrigin,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let uuid = serde_json::to_string(&cfd_offer.id).unwrap(); let uuid = serde_json::to_string(&cfd_offer.id).unwrap();
let trading_pair = serde_json::to_string(&cfd_offer.trading_pair).unwrap(); let trading_pair = serde_json::to_string(&cfd_offer.trading_pair).unwrap();
@ -27,6 +34,7 @@ pub async fn insert_cfd_offer(
let liquidation_price = serde_json::to_string(&cfd_offer.liquidation_price).unwrap(); let liquidation_price = serde_json::to_string(&cfd_offer.liquidation_price).unwrap();
let creation_timestamp = serde_json::to_string(&cfd_offer.creation_timestamp).unwrap(); let creation_timestamp = serde_json::to_string(&cfd_offer.creation_timestamp).unwrap();
let term = serde_json::to_string(&cfd_offer.term).unwrap(); let term = serde_json::to_string(&cfd_offer.term).unwrap();
let origin = serde_json::to_string(&origin).unwrap();
sqlx::query!( sqlx::query!(
r#" r#"
@ -40,8 +48,9 @@ pub async fn insert_cfd_offer(
leverage, leverage,
liquidation_price, liquidation_price,
creation_timestamp, creation_timestamp,
term term,
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?); origin
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
"#, "#,
uuid, uuid,
trading_pair, trading_pair,
@ -52,7 +61,8 @@ pub async fn insert_cfd_offer(
leverage, leverage,
liquidation_price, liquidation_price,
creation_timestamp, creation_timestamp,
term term,
origin
) )
.execute(conn) .execute(conn)
.await?; .await?;
@ -155,7 +165,7 @@ pub async fn insert_cfd(cfd: Cfd, conn: &mut PoolConnection<Sqlite>) -> anyhow::
Ok(()) Ok(())
} }
#[allow(dead_code)] // This is only used by one binary. #[allow(dead_code)]
pub async fn insert_new_cfd_state_by_offer_id( pub async fn insert_new_cfd_state_by_offer_id(
offer_id: CfdOfferId, offer_id: CfdOfferId,
new_state: CfdState, new_state: CfdState,
@ -190,6 +200,7 @@ pub async fn insert_new_cfd_state_by_offer_id(
Ok(()) Ok(())
} }
#[allow(dead_code)]
async fn load_cfd_id_by_offer_uuid( async fn load_cfd_id_by_offer_uuid(
offer_uuid: CfdOfferId, offer_uuid: CfdOfferId,
conn: &mut PoolConnection<Sqlite>, conn: &mut PoolConnection<Sqlite>,
@ -213,6 +224,7 @@ async fn load_cfd_id_by_offer_uuid(
Ok(cfd_id) Ok(cfd_id)
} }
#[allow(dead_code)]
async fn load_latest_cfd_state( async fn load_latest_cfd_state(
cfd_id: i64, cfd_id: i64,
conn: &mut PoolConnection<Sqlite>, conn: &mut PoolConnection<Sqlite>,
@ -250,6 +262,7 @@ pub async fn load_all_cfds(conn: &mut PoolConnection<Sqlite>) -> anyhow::Result<
offers.leverage as leverage, offers.leverage as leverage,
offers.trading_pair as trading_pair, offers.trading_pair as trading_pair,
offers.position as position, offers.position as position,
offers.origin as origin,
offers.liquidation_price as liquidation_price, offers.liquidation_price as liquidation_price,
cfds.quantity_usd as quantity_usd, cfds.quantity_usd as quantity_usd,
cfd_states.state as state cfd_states.state as state
@ -269,9 +282,6 @@ pub async fn load_all_cfds(conn: &mut PoolConnection<Sqlite>) -> anyhow::Result<
.fetch_all(conn) .fetch_all(conn)
.await?; .await?;
// TODO: We might want to separate the database model from the http model and properly map
// between them
let cfds = rows let cfds = rows
.iter() .iter()
.map(|row| { .map(|row| {
@ -279,11 +289,18 @@ pub async fn load_all_cfds(conn: &mut PoolConnection<Sqlite>) -> anyhow::Result<
let initial_price = serde_json::from_str(row.initial_price.as_str()).unwrap(); let initial_price = serde_json::from_str(row.initial_price.as_str()).unwrap();
let leverage = Leverage(row.leverage.try_into().unwrap()); let leverage = Leverage(row.leverage.try_into().unwrap());
let trading_pair = serde_json::from_str(row.trading_pair.as_str()).unwrap(); let trading_pair = serde_json::from_str(row.trading_pair.as_str()).unwrap();
let position = serde_json::from_str(row.position.as_str()).unwrap();
let liquidation_price = serde_json::from_str(row.liquidation_price.as_str()).unwrap(); let liquidation_price = serde_json::from_str(row.liquidation_price.as_str()).unwrap();
let quantity = serde_json::from_str(row.quantity_usd.as_str()).unwrap(); let quantity = serde_json::from_str(row.quantity_usd.as_str()).unwrap();
let latest_state = serde_json::from_str(row.state.as_str()).unwrap(); let latest_state = serde_json::from_str(row.state.as_str()).unwrap();
let origin: OfferOrigin = serde_json::from_str(row.origin.as_str()).unwrap();
let position: Position = serde_json::from_str(row.position.as_str()).unwrap();
let position = match origin {
OfferOrigin::Mine => position,
OfferOrigin::Others => position.counter_position(),
};
Cfd { Cfd {
offer_id, offer_id,
initial_price, initial_price,
@ -292,8 +309,6 @@ pub async fn load_all_cfds(conn: &mut PoolConnection<Sqlite>) -> anyhow::Result<
position, position,
liquidation_price, liquidation_price,
quantity_usd: quantity, quantity_usd: quantity,
profit_btc: Amount::ZERO,
profit_usd: Usd::ZERO,
state: latest_state, state: latest_state,
} }
}) })
@ -323,7 +338,9 @@ mod tests {
let mut conn = pool.acquire().await.unwrap(); let mut conn = pool.acquire().await.unwrap();
let cfd_offer = CfdOffer::from_default_with_price(Usd(dec!(10000))).unwrap(); let cfd_offer = CfdOffer::from_default_with_price(Usd(dec!(10000))).unwrap();
insert_cfd_offer(&cfd_offer, &mut conn).await.unwrap(); insert_cfd_offer(&cfd_offer, &mut conn, OfferOrigin::Others)
.await
.unwrap();
let cfd_offer_loaded = load_offer_by_id(cfd_offer.id, &mut conn).await.unwrap(); let cfd_offer_loaded = load_offer_by_id(cfd_offer.id, &mut conn).await.unwrap();
@ -344,12 +361,13 @@ mod tests {
transition_timestamp: SystemTime::now(), transition_timestamp: SystemTime::now(),
}, },
}, },
Usd(dec!(10001)), Position::Buy,
) );
.unwrap();
// the order ahs to exist in the db in order to be able to insert the cfd // the order ahs to exist in the db in order to be able to insert the cfd
insert_cfd_offer(&cfd_offer, &mut conn).await.unwrap(); insert_cfd_offer(&cfd_offer, &mut conn, OfferOrigin::Others)
.await
.unwrap();
insert_cfd(cfd.clone(), &mut conn).await.unwrap(); insert_cfd(cfd.clone(), &mut conn).await.unwrap();
let cfds_from_db = load_all_cfds(&mut conn).await.unwrap(); let cfds_from_db = load_all_cfds(&mut conn).await.unwrap();
@ -371,12 +389,13 @@ mod tests {
transition_timestamp: SystemTime::now(), transition_timestamp: SystemTime::now(),
}, },
}, },
Usd(dec!(10001)), Position::Buy,
) );
.unwrap();
// the order ahs to exist in the db in order to be able to insert the cfd // the order ahs to exist in the db in order to be able to insert the cfd
insert_cfd_offer(&cfd_offer, &mut conn).await.unwrap(); insert_cfd_offer(&cfd_offer, &mut conn, OfferOrigin::Others)
.await
.unwrap();
insert_cfd(cfd.clone(), &mut conn).await.unwrap(); insert_cfd(cfd.clone(), &mut conn).await.unwrap();
cfd.state = CfdState::Accepted { cfd.state = CfdState::Accepted {

33
daemon/src/maker_cfd_actor.rs

@ -1,10 +1,11 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::time::SystemTime; use std::time::SystemTime;
use crate::db::{insert_cfd, insert_cfd_offer, load_all_cfds, load_offer_by_id, OfferOrigin};
use crate::model::cfd::{Cfd, CfdOffer, CfdOfferId, CfdState, CfdStateCommon, FinalizedCfd}; use crate::model::cfd::{Cfd, CfdOffer, CfdOfferId, CfdState, CfdStateCommon, FinalizedCfd};
use crate::model::{TakerId, Usd}; use crate::model::{TakerId, Usd};
use crate::wire::{Msg0, Msg1, SetupMsg}; use crate::wire::{Msg0, Msg1, SetupMsg};
use crate::{db, maker_cfd_actor, maker_inc_connections_actor}; use crate::{maker_cfd_actor, maker_inc_connections_actor};
use bdk::bitcoin::secp256k1::{schnorrsig, SecretKey}; use bdk::bitcoin::secp256k1::{schnorrsig, SecretKey};
use bdk::bitcoin::{self, Amount}; use bdk::bitcoin::{self, Amount};
use bdk::database::BatchDatabase; use bdk::database::BatchDatabase;
@ -13,7 +14,6 @@ use cfd_protocol::{
WalletExt, WalletExt,
}; };
use futures::Future; use futures::Future;
use rust_decimal_macros::dec;
use tokio::sync::{mpsc, watch}; use tokio::sync::{mpsc, watch};
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
@ -62,7 +62,7 @@ where
// populate the CFD feed with existing CFDs // populate the CFD feed with existing CFDs
let mut conn = db.acquire().await.unwrap(); let mut conn = db.acquire().await.unwrap();
cfd_feed_actor_inbox cfd_feed_actor_inbox
.send(db::load_all_cfds(&mut conn).await.unwrap()) .send(load_all_cfds(&mut conn).await.unwrap())
.unwrap(); .unwrap();
while let Some(message) = receiver.recv().await { while let Some(message) = receiver.recv().await {
@ -82,9 +82,7 @@ where
// 1. Validate if offer is still valid // 1. Validate if offer is still valid
let current_offer = match current_offer_id { let current_offer = match current_offer_id {
Some(current_offer_id) if current_offer_id == offer_id => { Some(current_offer_id) if current_offer_id == offer_id => {
db::load_offer_by_id(current_offer_id, &mut conn) load_offer_by_id(current_offer_id, &mut conn).await.unwrap()
.await
.unwrap()
} }
_ => { _ => {
takers takers
@ -100,17 +98,16 @@ where
// 2. Insert CFD in DB // 2. Insert CFD in DB
// TODO: Don't auto-accept, present to user in UI instead // TODO: Don't auto-accept, present to user in UI instead
let cfd = Cfd::new( let cfd = Cfd::new(
current_offer, current_offer.clone(),
quantity, quantity,
CfdState::Accepted { CfdState::Accepted {
common: CfdStateCommon { common: CfdStateCommon {
transition_timestamp: SystemTime::now(), transition_timestamp: SystemTime::now(),
}, },
}, },
Usd(dec!(10001)), current_offer.position,
) );
.unwrap(); insert_cfd(cfd, &mut conn).await.unwrap();
db::insert_cfd(cfd, &mut conn).await.unwrap();
takers takers
.send(maker_inc_connections_actor::Command::NotifyOfferAccepted { .send(maker_inc_connections_actor::Command::NotifyOfferAccepted {
@ -119,7 +116,7 @@ where
}) })
.unwrap(); .unwrap();
cfd_feed_actor_inbox cfd_feed_actor_inbox
.send(db::load_all_cfds(&mut conn).await.unwrap()) .send(load_all_cfds(&mut conn).await.unwrap())
.unwrap(); .unwrap();
// 3. Remove current offer // 3. Remove current offer
@ -134,7 +131,9 @@ where
maker_cfd_actor::Command::NewOffer(offer) => { maker_cfd_actor::Command::NewOffer(offer) => {
// 1. Save to DB // 1. Save to DB
let mut conn = db.acquire().await.unwrap(); let mut conn = db.acquire().await.unwrap();
db::insert_cfd_offer(&offer, &mut conn).await.unwrap(); insert_cfd_offer(&offer, &mut conn, OfferOrigin::Mine)
.await
.unwrap();
// 2. Update actor state to current offer // 2. Update actor state to current offer
current_offer_id.replace(offer.id); current_offer_id.replace(offer.id);
@ -153,11 +152,9 @@ where
let mut conn = db.acquire().await.unwrap(); let mut conn = db.acquire().await.unwrap();
let current_offer = match current_offer_id { let current_offer = match current_offer_id {
Some(current_offer_id) => Some( Some(current_offer_id) => {
db::load_offer_by_id(current_offer_id, &mut conn) Some(load_offer_by_id(current_offer_id, &mut conn).await.unwrap())
.await }
.unwrap(),
),
None => None, None => None,
}; };

47
daemon/src/model.rs

@ -1,5 +1,7 @@
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use anyhow::{Context, Result};
use rust_decimal::prelude::ToPrimitive;
use rust_decimal::Decimal; use rust_decimal::Decimal;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -12,6 +14,33 @@ pub struct Usd(pub Decimal);
impl Usd { impl Usd {
pub const ZERO: Self = Self(Decimal::ZERO); pub const ZERO: Self = Self(Decimal::ZERO);
pub fn checked_add(&self, other: Usd) -> Result<Usd> {
let result = self.0.checked_add(other.0).context("addition error")?;
Ok(Usd(result))
}
pub fn checked_sub(&self, other: Usd) -> Result<Usd> {
let result = self.0.checked_sub(other.0).context("subtraction error")?;
Ok(Usd(result))
}
pub fn checked_mul(&self, other: Usd) -> Result<Usd> {
let result = self
.0
.checked_mul(other.0)
.context("multiplication error")?;
Ok(Usd(result))
}
pub fn checked_div(&self, other: Usd) -> Result<Usd> {
let result = self.0.checked_div(other.0).context("division error")?;
Ok(Usd(result))
}
pub fn try_into_u64(&self) -> Result<u64> {
self.0.to_u64().context("could not fit decimal into u64")
}
} }
impl Display for Usd { impl Display for Usd {
@ -20,7 +49,13 @@ impl Display for Usd {
} }
} }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] impl From<Decimal> for Usd {
fn from(decimal: Decimal) -> Self {
Usd(decimal)
}
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
pub struct Leverage(pub u8); pub struct Leverage(pub u8);
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
@ -34,6 +69,16 @@ pub enum Position {
Sell, Sell,
} }
impl Position {
#[allow(dead_code)]
pub fn counter_position(&self) -> Self {
match self {
Position::Buy => Position::Sell,
Position::Sell => Position::Buy,
}
}
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct TakerId(Uuid); pub struct TakerId(Uuid);

221
daemon/src/model/cfd.rs

@ -1,5 +1,5 @@
use crate::model::{Leverage, Position, TradingPair, Usd}; use crate::model::{Leverage, Position, TradingPair, Usd};
use anyhow::{Context, Result}; use anyhow::Result;
use bdk::bitcoin::secp256k1::{SecretKey, Signature}; use bdk::bitcoin::secp256k1::{SecretKey, Signature};
use bdk::bitcoin::util::psbt::PartiallySignedTransaction; use bdk::bitcoin::util::psbt::PartiallySignedTransaction;
use bdk::bitcoin::{Amount, Transaction}; use bdk::bitcoin::{Amount, Transaction};
@ -88,46 +88,18 @@ fn calculate_liquidation_price(
price: &Usd, price: &Usd,
maintenance_margin_rate: &Decimal, maintenance_margin_rate: &Decimal,
) -> Result<Usd> { ) -> Result<Usd> {
let leverage = Decimal::from(leverage.0); let leverage = Decimal::from(leverage.0).into();
let price = price.0; let maintenance_margin_rate: Usd = (*maintenance_margin_rate).into();
// liquidation price calc in isolated margin mode // liquidation price calc in isolated margin mode
// currently based on: https://help.bybit.com/hc/en-us/articles/360039261334-How-to-calculate-Liquidation-Price-Inverse-Contract- // currently based on: https://help.bybit.com/hc/en-us/articles/360039261334-How-to-calculate-Liquidation-Price-Inverse-Contract-
let liquidation_price = price let liquidation_price = price.checked_mul(leverage)?.checked_div(
.checked_mul(leverage) leverage
.context("multiplication error")? .checked_add(Decimal::ONE.into())?
.checked_div( .checked_sub(maintenance_margin_rate.checked_mul(leverage)?)?,
leverage )?;
.checked_add(Decimal::ONE)
.context("addition error")?
.checked_sub(
maintenance_margin_rate
.checked_mul(leverage)
.context("multiplication error")?,
)
.context("subtraction error")?,
)
.context("division error")?;
Ok(Usd(liquidation_price))
}
/// The taker POSTs this to create a Cfd
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CfdTakeRequest {
pub offer_id: CfdOfferId,
pub quantity: Usd,
}
/// The maker POSTs this to create a new CfdOffer Ok(liquidation_price)
// TODO: Use Rocket form?
#[derive(Debug, Clone, Deserialize)]
pub struct CfdNewOfferRequest {
pub price: Usd,
// TODO: [post-MVP] Representation of the contract size; at the moment the contract size is
// always 1 USD
pub min_quantity: Usd,
pub max_quantity: Usd,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@ -182,6 +154,8 @@ pub enum CfdState {
Open { Open {
common: CfdStateCommon, common: CfdStateCommon,
settlement_timestamp: SystemTime, settlement_timestamp: SystemTime,
#[serde(with = "::bdk::bitcoin::util::amount::serde::as_sat")]
margin: Amount,
}, },
/// Requested close the position, but we have not passed that on to the blockchain yet. /// Requested close the position, but we have not passed that on to the blockchain yet.
@ -204,26 +178,26 @@ pub enum CfdState {
} }
impl CfdState { impl CfdState {
// fn get_common(&self) -> CfdStateCommon { fn get_common(&self) -> CfdStateCommon {
// let common = match self { let common = match self {
// CfdState::TakeRequested { common } => common, CfdState::TakeRequested { common } => common,
// CfdState::PendingTakeRequest { common } => common, CfdState::PendingTakeRequest { common } => common,
// CfdState::Accepted { common } => common, CfdState::Accepted { common } => common,
// CfdState::Rejected { common } => common, CfdState::Rejected { common } => common,
// CfdState::ContractSetup { common } => common, CfdState::ContractSetup { common } => common,
// CfdState::Open { common, .. } => common, CfdState::Open { common, .. } => common,
// CfdState::CloseRequested { common } => common, CfdState::CloseRequested { common } => common,
// CfdState::PendingClose { common } => common, CfdState::PendingClose { common } => common,
// CfdState::Closed { common } => common, CfdState::Closed { common } => common,
// CfdState::Error { common } => common, CfdState::Error { common } => common,
// }; };
// *common *common
// } }
// pub fn get_transition_timestamp(&self) -> SystemTime { pub fn get_transition_timestamp(&self) -> SystemTime {
// self.get_common().transition_timestamp self.get_common().transition_timestamp
// } }
} }
impl Display for CfdState { impl Display for CfdState {
@ -276,36 +250,42 @@ pub struct Cfd {
pub quantity_usd: Usd, pub quantity_usd: Usd,
#[serde(with = "::bdk::bitcoin::util::amount::serde::as_btc")]
pub profit_btc: Amount,
pub profit_usd: Usd,
pub state: CfdState, pub state: CfdState,
} }
impl Cfd { impl Cfd {
pub fn new( pub fn new(cfd_offer: CfdOffer, quantity: Usd, state: CfdState, position: Position) -> Self {
cfd_offer: CfdOffer, Cfd {
quantity: Usd,
state: CfdState,
current_price: Usd,
) -> Result<Self> {
let (profit_btc, profit_usd) =
calculate_profit(cfd_offer.price, current_price, dec!(0.005), Usd(dec!(0.1)))?;
Ok(Cfd {
offer_id: cfd_offer.id, offer_id: cfd_offer.id,
initial_price: cfd_offer.price, initial_price: cfd_offer.price,
leverage: cfd_offer.leverage, leverage: cfd_offer.leverage,
trading_pair: cfd_offer.trading_pair, trading_pair: cfd_offer.trading_pair,
position: cfd_offer.position, position,
liquidation_price: cfd_offer.liquidation_price, liquidation_price: cfd_offer.liquidation_price,
quantity_usd: quantity, quantity_usd: quantity,
// initially the profit is zero
profit_btc,
profit_usd,
state, state,
}) }
}
pub fn calc_margin(&self) -> Result<Amount> {
let margin = match self.position {
Position::Buy => {
calculate_buy_margin(self.initial_price, self.quantity_usd, self.leverage)?
}
Position::Sell => calculate_sell_margin(self.initial_price, self.quantity_usd)?,
};
Ok(margin)
}
pub fn calc_profit(&self, current_price: Usd) -> Result<(Amount, Usd)> {
let profit = calculate_profit(
self.initial_price,
current_price,
dec!(0.005),
Usd(dec!(0.1)),
)?;
Ok(profit)
} }
} }
@ -319,6 +299,38 @@ fn calculate_profit(
Ok((Amount::ZERO, Usd::ZERO)) Ok((Amount::ZERO, Usd::ZERO))
} }
/// Calculates the buyer'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: Usd, quantity: Usd, leverage: Leverage) -> Result<Amount> {
let leverage = Decimal::from(leverage.0).into();
let margin = quantity.checked_div(price.checked_mul(leverage)?)?;
let sat_adjust = Decimal::from(Amount::ONE_BTC.as_sat()).into();
let margin = margin.checked_mul(sat_adjust)?;
let margin = Amount::from_sat(margin.try_into_u64()?);
Ok(margin)
}
/// Calculates the seller'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: Usd, quantity: Usd) -> Result<Amount> {
let margin = quantity.checked_div(price)?;
let sat_adjust = Decimal::from(Amount::ONE_BTC.as_sat()).into();
let margin = margin.checked_mul(sat_adjust)?;
let margin = Amount::from_sat(margin.try_into_u64()?);
Ok(margin)
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -337,9 +349,61 @@ mod tests {
assert_eq!(liquidation_price, Usd(dec!(41004.184100418410041841004184))); assert_eq!(liquidation_price, Usd(dec!(41004.184100418410041841004184)));
} }
#[test]
fn given_leverage_of_one_and_equal_price_and_quantity_then_buy_margin_is_one_btc() {
let price = Usd(dec!(40000));
let quantity = Usd(dec![40000]);
let leverage = Leverage(1);
let buy_margin = calculate_buy_margin(price, quantity, leverage).unwrap();
assert_eq!(buy_margin, Amount::ONE_BTC);
}
#[test]
fn given_leverage_of_one_and_leverage_of_ten_then_buy_margin_is_lower_factor_ten() {
let price = Usd(dec!(40000));
let quantity = Usd(dec![40000]);
let leverage = Leverage(10);
let buy_margin = calculate_buy_margin(price, quantity, leverage).unwrap();
assert_eq!(buy_margin, Amount::from_btc(0.1).unwrap());
}
#[test]
fn given_quantity_equals_price_then_sell_margin_is_one_btc() {
let price = Usd(dec!(40000));
let quantity = Usd(dec![40000]);
let sell_margin = calculate_sell_margin(price, quantity).unwrap();
assert_eq!(sell_margin, Amount::ONE_BTC);
}
#[test]
fn given_quantity_half_of_price_then_sell_margin_is_half_btc() {
let price = Usd(dec!(40000));
let quantity = Usd(dec![20000]);
let sell_margin = calculate_sell_margin(price, quantity).unwrap();
assert_eq!(sell_margin, Amount::from_btc(0.5).unwrap());
}
#[test]
fn given_quantity_double_of_price_then_sell_margin_is_two_btc() {
let price = Usd(dec!(40000));
let quantity = Usd(dec![80000]);
let sell_margin = calculate_sell_margin(price, quantity).unwrap();
assert_eq!(sell_margin, Amount::from_btc(2.0).unwrap());
}
#[test] #[test]
fn serialize_cfd_state_snapshot() { fn serialize_cfd_state_snapshot() {
// This test is to prevent us from breaking the cfd_state API used by the UI and database! // This test is to prevent us from breaking the CfdState API against the database.
// We serialize the state into the database, so changes to the enum result in breaking // We serialize the state into the database, so changes to the enum result in breaking
// program version changes. // program version changes.
@ -394,11 +458,12 @@ mod tests {
transition_timestamp: fixed_timestamp, transition_timestamp: fixed_timestamp,
}, },
settlement_timestamp: fixed_timestamp, settlement_timestamp: fixed_timestamp,
margin: Amount::from_btc(0.5).unwrap(),
}; };
let json = serde_json::to_string(&cfd_state).unwrap(); let json = serde_json::to_string(&cfd_state).unwrap();
assert_eq!( assert_eq!(
json, json,
r#"{"type":"Open","payload":{"common":{"transition_timestamp":{"secs_since_epoch":0,"nanos_since_epoch":0}},"settlement_timestamp":{"secs_since_epoch":0,"nanos_since_epoch":0}}}"# r#"{"type":"Open","payload":{"common":{"transition_timestamp":{"secs_since_epoch":0,"nanos_since_epoch":0}},"settlement_timestamp":{"secs_since_epoch":0,"nanos_since_epoch":0},"margin":50000000}}"#
); );
let cfd_state = CfdState::CloseRequested { let cfd_state = CfdState::CloseRequested {

15
daemon/src/routes_maker.rs

@ -1,5 +1,6 @@
use crate::maker_cfd_actor; use crate::maker_cfd_actor;
use crate::model::cfd::{Cfd, CfdNewOfferRequest, CfdOffer}; use crate::model::cfd::{Cfd, CfdOffer};
use crate::model::Usd;
use crate::to_sse_event::ToSseEvent; use crate::to_sse_event::ToSseEvent;
use anyhow::Result; use anyhow::Result;
use bdk::bitcoin::Amount; use bdk::bitcoin::Amount;
@ -7,6 +8,7 @@ use rocket::response::status;
use rocket::response::stream::EventStream; use rocket::response::stream::EventStream;
use rocket::serde::json::Json; use rocket::serde::json::Json;
use rocket::State; use rocket::State;
use serde::Deserialize;
use tokio::select; use tokio::select;
use tokio::sync::{mpsc, watch}; use tokio::sync::{mpsc, watch};
@ -49,6 +51,17 @@ pub async fn maker_feed(
} }
} }
/// The maker POSTs this to create a new CfdOffer
// TODO: Use Rocket form?
#[derive(Debug, Clone, Deserialize)]
pub struct CfdNewOfferRequest {
pub price: Usd,
// TODO: [post-MVP] Representation of the contract size; at the moment the contract size is
// always 1 USD
pub min_quantity: Usd,
pub max_quantity: Usd,
}
#[rocket::post("/offer/sell", data = "<offer>")] #[rocket::post("/offer/sell", data = "<offer>")]
pub async fn post_sell_offer( pub async fn post_sell_offer(
offer: Json<CfdNewOfferRequest>, offer: Json<CfdNewOfferRequest>,

41
daemon/src/routes_taker.rs

@ -1,10 +1,13 @@
use crate::model::cfd::{Cfd, CfdOffer, CfdTakeRequest}; use crate::model::cfd::{calculate_buy_margin, Cfd, CfdOffer, CfdOfferId};
use crate::model::{Leverage, Usd};
use crate::taker_cfd_actor; use crate::taker_cfd_actor;
use crate::to_sse_event::ToSseEvent; use crate::to_sse_event::ToSseEvent;
use bdk::bitcoin::Amount; use bdk::bitcoin::Amount;
use rocket::response::status;
use rocket::response::stream::EventStream; use rocket::response::stream::EventStream;
use rocket::serde::json::Json; use rocket::serde::json::Json;
use rocket::State; use rocket::State;
use serde::{Deserialize, Serialize};
use tokio::select; use tokio::select;
use tokio::sync::{mpsc, watch}; use tokio::sync::{mpsc, watch};
@ -47,6 +50,12 @@ pub async fn feed(
} }
} }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CfdTakeRequest {
pub offer_id: CfdOfferId,
pub quantity: Usd,
}
#[rocket::post("/cfd", data = "<cfd_take_request>")] #[rocket::post("/cfd", data = "<cfd_take_request>")]
pub async fn post_cfd( pub async fn post_cfd(
cfd_take_request: Json<CfdTakeRequest>, cfd_take_request: Json<CfdTakeRequest>,
@ -62,3 +71,33 @@ pub async fn post_cfd(
#[rocket::get("/alive")] #[rocket::get("/alive")]
pub fn get_health_check() {} pub fn get_health_check() {}
#[derive(Debug, Clone, Copy, Deserialize)]
pub struct MarginRequest {
pub price: Usd,
pub quantity: Usd,
pub leverage: Leverage,
}
/// Represents the collateral that has to be put up
#[derive(Debug, Clone, Copy, Serialize)]
pub struct MarginResponse {
#[serde(with = "::bdk::bitcoin::util::amount::serde::as_btc")]
pub margin: Amount,
}
// TODO: Consider moving this into wasm and load it into the UI instead of triggering this endpoint
// upon every quantity keystroke
#[rocket::post("/calculate/margin", data = "<margin_request>")]
pub fn margin_calc(
margin_request: Json<MarginRequest>,
) -> Result<status::Accepted<Json<MarginResponse>>, status::BadRequest<String>> {
let margin = calculate_buy_margin(
margin_request.price,
margin_request.quantity,
margin_request.leverage,
)
.map_err(|e| status::BadRequest(Some(e.to_string())))?;
Ok(status::Accepted(Some(Json(MarginResponse { margin }))))
}

3
daemon/src/taker.rs

@ -112,7 +112,8 @@ async fn main() -> Result<()> {
rocket::routes![ rocket::routes![
routes_taker::feed, routes_taker::feed,
routes_taker::post_cfd, routes_taker::post_cfd,
routes_taker::get_health_check routes_taker::get_health_check,
routes_taker::margin_calc,
], ],
) )
.launch() .launch()

31
daemon/src/taker_cfd_actor.rs

@ -1,7 +1,11 @@
use crate::db::{
insert_cfd, insert_cfd_offer, insert_new_cfd_state_by_offer_id, load_all_cfds,
load_offer_by_id, OfferOrigin,
};
use crate::model::cfd::{Cfd, CfdOffer, CfdOfferId, CfdState, CfdStateCommon, FinalizedCfd}; use crate::model::cfd::{Cfd, CfdOffer, CfdOfferId, CfdState, CfdStateCommon, FinalizedCfd};
use crate::model::Usd; use crate::model::Usd;
use crate::wire;
use crate::wire::{Msg0, Msg1, SetupMsg}; use crate::wire::{Msg0, Msg1, SetupMsg};
use crate::{db, wire};
use bdk::bitcoin::secp256k1::{schnorrsig, SecretKey}; use bdk::bitcoin::secp256k1::{schnorrsig, SecretKey};
use bdk::bitcoin::{self, Amount}; use bdk::bitcoin::{self, Amount};
@ -47,7 +51,7 @@ where
// populate the CFD feed with existing CFDs // populate the CFD feed with existing CFDs
let mut conn = db.acquire().await.unwrap(); let mut conn = db.acquire().await.unwrap();
cfd_feed_actor_inbox cfd_feed_actor_inbox
.send(db::load_all_cfds(&mut conn).await.unwrap()) .send(load_all_cfds(&mut conn).await.unwrap())
.unwrap(); .unwrap();
while let Some(message) = receiver.recv().await { while let Some(message) = receiver.recv().await {
@ -55,27 +59,25 @@ where
Command::TakeOffer { offer_id, quantity } => { Command::TakeOffer { offer_id, quantity } => {
let mut conn = db.acquire().await.unwrap(); let mut conn = db.acquire().await.unwrap();
let current_offer = let current_offer = load_offer_by_id(offer_id, &mut conn).await.unwrap();
db::load_offer_by_id(offer_id, &mut conn).await.unwrap();
println!("Accepting current offer: {:?}", &current_offer); println!("Accepting current offer: {:?}", &current_offer);
let cfd = Cfd::new( let cfd = Cfd::new(
current_offer, current_offer.clone(),
quantity, quantity,
CfdState::PendingTakeRequest { CfdState::PendingTakeRequest {
common: CfdStateCommon { common: CfdStateCommon {
transition_timestamp: SystemTime::now(), transition_timestamp: SystemTime::now(),
}, },
}, },
Usd::ZERO, current_offer.position.counter_position(),
) );
.unwrap();
db::insert_cfd(cfd, &mut conn).await.unwrap(); insert_cfd(cfd, &mut conn).await.unwrap();
cfd_feed_actor_inbox cfd_feed_actor_inbox
.send(db::load_all_cfds(&mut conn).await.unwrap()) .send(load_all_cfds(&mut conn).await.unwrap())
.unwrap(); .unwrap();
out_msg_maker_inbox out_msg_maker_inbox
.send(wire::TakerToMaker::TakeOffer { offer_id, quantity }) .send(wire::TakerToMaker::TakeOffer { offer_id, quantity })
@ -83,15 +85,18 @@ where
} }
Command::NewOffer(Some(offer)) => { Command::NewOffer(Some(offer)) => {
let mut conn = db.acquire().await.unwrap(); let mut conn = db.acquire().await.unwrap();
db::insert_cfd_offer(&offer, &mut conn).await.unwrap(); insert_cfd_offer(&offer, &mut conn, OfferOrigin::Others)
.await
.unwrap();
offer_feed_actor_inbox.send(Some(offer)).unwrap(); offer_feed_actor_inbox.send(Some(offer)).unwrap();
} }
Command::NewOffer(None) => { Command::NewOffer(None) => {
offer_feed_actor_inbox.send(None).unwrap(); offer_feed_actor_inbox.send(None).unwrap();
} }
Command::OfferAccepted(offer_id) => { Command::OfferAccepted(offer_id) => {
let mut conn = db.acquire().await.unwrap(); let mut conn = db.acquire().await.unwrap();
db::insert_new_cfd_state_by_offer_id( insert_new_cfd_state_by_offer_id(
offer_id, offer_id,
CfdState::ContractSetup { CfdState::ContractSetup {
common: CfdStateCommon { common: CfdStateCommon {
@ -104,7 +109,7 @@ where
.unwrap(); .unwrap();
cfd_feed_actor_inbox cfd_feed_actor_inbox
.send(db::load_all_cfds(&mut conn).await.unwrap()) .send(load_all_cfds(&mut conn).await.unwrap())
.unwrap(); .unwrap();
let (sk, pk) = crate::keypair::new(&mut rand::thread_rng()); let (sk, pk) = crate::keypair::new(&mut rand::thread_rng());

106
daemon/src/to_sse_event.rs

@ -1,20 +1,116 @@
use crate::model::cfd::{Cfd, CfdOffer}; use crate::model;
use crate::model::cfd::CfdOfferId;
use crate::model::{Leverage, Position, TradingPair, Usd};
use bdk::bitcoin::Amount; use bdk::bitcoin::Amount;
use rocket::response::stream::Event; use rocket::response::stream::Event;
use serde::Serialize;
use std::time::UNIX_EPOCH;
#[derive(Debug, Clone, Serialize)]
pub struct Cfd {
pub offer_id: CfdOfferId,
pub initial_price: Usd,
pub leverage: Leverage,
pub trading_pair: TradingPair,
pub position: Position,
pub liquidation_price: Usd,
pub quantity_usd: Usd,
#[serde(with = "::bdk::bitcoin::util::amount::serde::as_btc")]
pub margin: Amount,
#[serde(with = "::bdk::bitcoin::util::amount::serde::as_btc")]
pub profit_btc: Amount,
pub profit_usd: Usd,
pub state: String,
pub state_transition_unix_timestamp: u64,
}
#[derive(Debug, Clone, Serialize)]
pub struct CfdOffer {
pub id: CfdOfferId,
pub trading_pair: TradingPair,
pub position: Position,
pub price: Usd,
pub min_quantity: Usd,
pub max_quantity: Usd,
pub leverage: Leverage,
pub liquidation_price: Usd,
pub creation_unix_timestamp: u64,
pub term_in_secs: u64,
}
pub trait ToSseEvent { pub trait ToSseEvent {
fn to_sse_event(&self) -> Event; fn to_sse_event(&self) -> Event;
} }
impl ToSseEvent for Vec<Cfd> { impl ToSseEvent for Vec<model::cfd::Cfd> {
// TODO: This conversion can fail, we might want to change the API
fn to_sse_event(&self) -> Event { fn to_sse_event(&self) -> Event {
Event::json(self).event("cfds") let cfds = self
.iter()
.map(|cfd| {
// TODO: Get the actual current price here
let current_price = Usd::ZERO;
let (profit_btc, profit_usd) = cfd.calc_profit(current_price).unwrap();
Cfd {
offer_id: cfd.offer_id,
initial_price: cfd.initial_price,
leverage: cfd.leverage,
trading_pair: cfd.trading_pair.clone(),
position: cfd.position.clone(),
liquidation_price: cfd.liquidation_price,
quantity_usd: cfd.quantity_usd,
profit_btc,
profit_usd,
state: cfd.state.to_string(),
state_transition_unix_timestamp: cfd
.state
.get_transition_timestamp()
.duration_since(UNIX_EPOCH)
.expect("timestamp to be convertable to duration since epoch")
.as_secs(),
// TODO: Depending on the state the margin might be set (i.e. in Open we save it
// in the DB internally) and does not have to be calculated
margin: cfd.calc_margin().unwrap(),
}
})
.collect::<Vec<Cfd>>();
Event::json(&cfds).event("cfds")
} }
} }
impl ToSseEvent for Option<CfdOffer> { impl ToSseEvent for Option<model::cfd::CfdOffer> {
fn to_sse_event(&self) -> Event { fn to_sse_event(&self) -> Event {
Event::json(self).event("offer") let offer = self.clone().map(|offer| CfdOffer {
id: offer.id,
trading_pair: offer.trading_pair,
position: offer.position,
price: offer.price,
min_quantity: offer.min_quantity,
max_quantity: offer.max_quantity,
leverage: offer.leverage,
liquidation_price: offer.liquidation_price,
creation_unix_timestamp: offer
.creation_timestamp
.duration_since(UNIX_EPOCH)
.expect("timestamp to be convertiblae to dureation since epoch")
.as_secs(),
term_in_secs: offer.term.as_secs(),
});
Event::json(&offer).event("offer")
} }
} }

63
frontend/src/Taker.tsx

@ -19,6 +19,16 @@ interface CfdTakeRequestPayload {
quantity: number; quantity: number;
} }
interface MarginRequestPayload {
price: number;
quantity: number;
leverage: number;
}
interface MarginResponse {
margin: number;
}
async function postCfdTakeRequest(payload: CfdTakeRequestPayload) { async function postCfdTakeRequest(payload: CfdTakeRequestPayload) {
let res = await axios.post(BASE_URL + `/cfd`, JSON.stringify(payload)); let res = await axios.post(BASE_URL + `/cfd`, JSON.stringify(payload));
@ -27,6 +37,16 @@ async function postCfdTakeRequest(payload: CfdTakeRequestPayload) {
} }
} }
async function getMargin(payload: MarginRequestPayload): Promise<MarginResponse> {
let res = await axios.post(BASE_URL + `/calculate/margin`, JSON.stringify(payload));
if (!res.status.toString().startsWith("2")) {
throw new Error("failed to create new CFD take request: " + res.status + ", " + res.statusText);
}
return res.data;
}
export default function App() { export default function App() {
let source = useEventSource({ source: BASE_URL + "/feed" }); let source = useEventSource({ source: BASE_URL + "/feed" });
@ -35,7 +55,28 @@ export default function App() {
const balance = useLatestEvent<number>(source, "balance"); const balance = useLatestEvent<number>(source, "balance");
const toast = useToast(); const toast = useToast();
let [quantity, setQuantity] = useState<string>("10000"); let [quantity, setQuantity] = useState("0");
let [margin, setMargin] = useState("0");
let { run: calculateMargin } = useAsync({
deferFn: async ([payload]: any[]) => {
try {
let res = await getMargin(payload as MarginRequestPayload);
setMargin(res.margin.toString());
} catch (e) {
const description = typeof e === "string" ? e : JSON.stringify(e);
toast({
title: "Error",
description,
status: "error",
duration: 9000,
isClosable: true,
});
}
},
});
const format = (val: any) => `$` + val; const format = (val: any) => `$` + val;
const parse = (val: any) => val.replace(/^\$/, ""); const parse = (val: any) => val.replace(/^\$/, "");
@ -110,10 +151,28 @@ export default function App() {
<HStack> <HStack>
<Text>Quantity:</Text> <Text>Quantity:</Text>
<CurrencyInputField <CurrencyInputField
onChange={(valueString: string) => setQuantity(parse(valueString))} onChange={(valueString: string) => {
setQuantity(parse(valueString))
if (!offer) {
return;
}
let quantity = valueString ? Number.parseFloat(valueString) : 0;
let payload: MarginRequestPayload = {
leverage: offer.leverage,
price: offer.price,
quantity
}
calculateMargin(payload);
}}
value={format(quantity)} value={format(quantity)}
/> />
</HStack> </HStack>
<HStack>
<Text>Margin in BTC:</Text>
<Text>{margin}</Text>
</HStack>
<Text>Leverage:</Text> <Text>Leverage:</Text>
{/* TODO: consider button group */} {/* TODO: consider button group */}
<Flex justifyContent={"space-between"}> <Flex justifyContent={"space-between"}>

14
frontend/src/components/CfdTile.tsx

@ -24,8 +24,14 @@ export default function CfdTile(
<Text>{cfd.trading_pair}</Text> <Text>{cfd.trading_pair}</Text>
<Text>Position</Text> <Text>Position</Text>
<Text>{cfd.position}</Text> <Text>{cfd.position}</Text>
<Text>Amount</Text> <Text>CFD Price</Text>
<Text>{cfd.initial_price}</Text>
<Text>Leverage</Text>
<Text>{cfd.leverage}</Text>
<Text>Quantity</Text>
<Text>{cfd.quantity_usd}</Text> <Text>{cfd.quantity_usd}</Text>
<Text>Margin</Text>
<Text>{cfd.margin}</Text>
<Text>Liquidation Price</Text> <Text>Liquidation Price</Text>
<Text <Text
overflow="hidden" overflow="hidden"
@ -40,12 +46,12 @@ export default function CfdTile(
<Text>Open since</Text> <Text>Open since</Text>
{/* TODO: Format date in a more compact way */} {/* TODO: Format date in a more compact way */}
<Text> <Text>
{(new Date(cfd.state.payload.common.transition_timestamp.secs_since_epoch * 1000).toString())} {(new Date(cfd.state_transition_unix_timestamp * 1000).toString())}
</Text> </Text>
<Text>Status</Text> <Text>Status</Text>
<Text>{cfd.state.type}</Text> <Text>{cfd.state}</Text>
</SimpleGrid> </SimpleGrid>
{cfd.state.type === "Open" {cfd.state === "Open"
&& <Box paddingBottom={5}><Button colorScheme="blue" variant="solid">Close</Button></Box>} && <Box paddingBottom={5}><Button colorScheme="blue" variant="solid">Close</Button></Box>}
</VStack> </VStack>
</Box> </Box>

34
frontend/src/components/Types.tsx

@ -1,13 +1,3 @@
export interface RustDuration {
secs: number;
nanos: number;
}
export interface RustTimestamp {
secs_since_epoch: number;
nanos_since_epoch: number;
}
export interface Offer { export interface Offer {
id: string; id: string;
trading_pair: string; trading_pair: string;
@ -17,22 +7,8 @@ export interface Offer {
max_quantity: number; max_quantity: number;
leverage: number; leverage: number;
liquidation_price: number; liquidation_price: number;
creation_timestamp: RustTimestamp; creation_unix_timestamp: number;
term: RustDuration; term_in_secs: number;
}
export interface CfdStateCommon {
transition_timestamp: RustTimestamp;
}
export interface CfdStatePayload {
common: CfdStateCommon;
settlement_timestamp?: RustTimestamp; // only in state Open
}
export interface CfdState {
type: string;
payload: CfdStatePayload;
} }
export interface Cfd { export interface Cfd {
@ -45,8 +21,12 @@ export interface Cfd {
liquidation_price: number; liquidation_price: number;
quantity_usd: number; quantity_usd: number;
margin: number;
profit_btc: number; profit_btc: number;
profit_usd: number; profit_usd: number;
state: CfdState; state: string;
state_transition_unix_timestamp: number;
} }

Loading…
Cancel
Save