Skip to content

Commit a99d12b

Browse files
Add a configurable spread to the ASB
Fixes #381.
1 parent 3e0301a commit a99d12b

File tree

6 files changed

+109
-20
lines changed

6 files changed

+109
-20
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717
- A changelog file.
1818
- Automatic resume of unfinished swaps for the `asb` upon startup.
1919
Unfinished swaps from earlier versions will be skipped.
20+
- A configurable spread for the ASB that is applied to the asking price received from the Kraken price ticker.
21+
The default value is 2% and can be configured using the `--ask-spread` parameter.
22+
See `./asb --help` for details.
2023

2124
### Fixed
2225

swap/src/asb/command.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::bitcoin::Amount;
22
use bitcoin::util::amount::ParseAmountError;
33
use bitcoin::{Address, Denomination};
4+
use rust_decimal::Decimal;
45
use std::path::PathBuf;
56

67
#[derive(structopt::StructOpt, Debug)]
@@ -27,6 +28,12 @@ pub enum Command {
2728
Start {
2829
#[structopt(long = "max-buy-btc", help = "The maximum amount of BTC the ASB is willing to buy.", default_value="0.005", parse(try_from_str = parse_btc))]
2930
max_buy: Amount,
31+
#[structopt(
32+
long = "ask-spread",
33+
help = "The spread in percent that should be applied to the asking price.",
34+
default_value = "0.02"
35+
)]
36+
ask_spread: Decimal,
3037
},
3138
History,
3239
WithdrawBtc {

swap/src/asb/rate.rs

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,47 @@ use rust_decimal::prelude::ToPrimitive;
44
use rust_decimal::Decimal;
55
use std::fmt::{Debug, Display, Formatter};
66

7-
/// Prices at which 1 XMR will be traded, in BTC (XMR/BTC pair)
8-
/// The `ask` represents the minimum price in BTC for which we are willing to
9-
/// sell 1 XMR.
7+
/// Represents the rate at which we are willing to trade 1 XMR.
108
#[derive(Debug, Clone, Copy, PartialEq)]
119
pub struct Rate {
10+
/// Represents the asking price from the market.
1211
ask: bitcoin::Amount,
12+
/// The spread which should be applied to the market asking price.
13+
ask_spread: Decimal,
1314
}
1415

16+
const ZERO_SPREAD: Decimal = Decimal::from_parts(0, 0, 0, false, 0);
17+
1518
impl Rate {
1619
pub const ZERO: Rate = Rate {
1720
ask: bitcoin::Amount::ZERO,
21+
ask_spread: ZERO_SPREAD,
1822
};
1923

20-
pub fn new(ask: bitcoin::Amount) -> Self {
21-
Self { ask }
24+
pub fn new(ask: bitcoin::Amount, ask_spread: Decimal) -> Self {
25+
Self { ask, ask_spread }
2226
}
2327

24-
pub fn ask(&self) -> bitcoin::Amount {
25-
self.ask
28+
/// Computes the asking price at which we are willing to sell 1 XMR.
29+
///
30+
/// This applies the spread to the market asking price.
31+
pub fn ask(&self) -> Result<bitcoin::Amount> {
32+
let sats = self.ask.as_sat();
33+
let sats = Decimal::from(sats);
34+
35+
let additional_sats = sats * self.ask_spread;
36+
let additional_sats = bitcoin::Amount::from_sat(
37+
additional_sats
38+
.to_u64()
39+
.context("Failed to fit spread into u64")?,
40+
);
41+
42+
Ok(self.ask + additional_sats)
2643
}
2744

28-
// This function takes the quote amount as it is what Bob sends to Alice in the
29-
// swap request
45+
/// Calculate a sell quote for a given BTC amount.
3046
pub fn sell_quote(&self, quote: bitcoin::Amount) -> Result<monero::Amount> {
31-
Self::quote(self.ask, quote)
47+
Self::quote(self.ask()?, quote)
3248
}
3349

3450
fn quote(rate: bitcoin::Amount, quote: bitcoin::Amount) -> Result<monero::Amount> {
@@ -67,16 +83,51 @@ impl Display for Rate {
6783
mod tests {
6884
use super::*;
6985

86+
const TWO_PERCENT: Decimal = Decimal::from_parts(2, 0, 0, false, 2);
87+
const ONE: Decimal = Decimal::from_parts(1, 0, 0, false, 0);
88+
7089
#[test]
7190
fn sell_quote() {
72-
let rate = Rate {
73-
ask: bitcoin::Amount::from_btc(0.002_500).unwrap(),
74-
};
91+
let asking_price = bitcoin::Amount::from_btc(0.002_500).unwrap();
92+
let rate = Rate::new(asking_price, ZERO_SPREAD);
7593

7694
let btc_amount = bitcoin::Amount::from_btc(2.5).unwrap();
7795

7896
let xmr_amount = rate.sell_quote(btc_amount).unwrap();
7997

8098
assert_eq!(xmr_amount, monero::Amount::from_monero(1000.0).unwrap())
8199
}
100+
101+
#[test]
102+
fn applies_spread_to_asking_price() {
103+
let asking_price = bitcoin::Amount::from_sat(100);
104+
let rate = Rate::new(asking_price, TWO_PERCENT);
105+
106+
let amount = rate.ask().unwrap();
107+
108+
assert_eq!(amount.as_sat(), 102);
109+
}
110+
111+
#[test]
112+
fn given_spread_of_two_percent_when_caluclating_sell_quote_factor_between_should_be_two_percent(
113+
) {
114+
let asking_price = bitcoin::Amount::from_btc(0.004).unwrap();
115+
116+
let rate_no_spread = Rate::new(asking_price, ZERO_SPREAD);
117+
let rate_with_spread = Rate::new(asking_price, TWO_PERCENT);
118+
119+
let xmr_no_spread = rate_no_spread.sell_quote(bitcoin::Amount::ONE_BTC).unwrap();
120+
let xmr_with_spread = rate_with_spread
121+
.sell_quote(bitcoin::Amount::ONE_BTC)
122+
.unwrap();
123+
124+
let xmr_factor =
125+
xmr_no_spread.as_piconero_decimal() / xmr_with_spread.as_piconero_decimal() - ONE;
126+
127+
assert!(xmr_with_spread < xmr_no_spread);
128+
assert_eq!(xmr_factor.round_dp(8), TWO_PERCENT); // round to 8 decimal
129+
// places to show that
130+
// it is really close
131+
// to two percent
132+
}
82133
}

swap/src/bin/asb.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use swap::env::GetConfig;
2626
use swap::fs::default_config_path;
2727
use swap::monero::Amount;
2828
use swap::network::swarm;
29+
use swap::protocol::alice::event_loop::KrakenRate;
2930
use swap::protocol::alice::{run, Behaviour, EventLoop};
3031
use swap::seed::Seed;
3132
use swap::trace::init_tracing;
@@ -74,7 +75,10 @@ async fn main() -> Result<()> {
7475
let env_config = env::Testnet::get_config();
7576

7677
match opt.cmd {
77-
Command::Start { max_buy } => {
78+
Command::Start {
79+
max_buy,
80+
ask_spread,
81+
} => {
7882
let bitcoin_wallet = init_bitcoin_wallet(&config, &seed, env_config).await?;
7983
let monero_wallet = init_monero_wallet(&config, env_config).await?;
8084

@@ -104,7 +108,7 @@ async fn main() -> Result<()> {
104108
Arc::new(bitcoin_wallet),
105109
Arc::new(monero_wallet),
106110
Arc::new(db),
107-
kraken_price_updates,
111+
KrakenRate::new(ask_spread, kraken_price_updates),
108112
max_buy,
109113
)
110114
.unwrap();

swap/src/monero.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,10 @@ impl Amount {
9696
Self::from_decimal(decimal)
9797
}
9898

99+
pub fn as_piconero_decimal(&self) -> Decimal {
100+
Decimal::from(self.as_piconero())
101+
}
102+
99103
fn from_decimal(amount: Decimal) -> Result<Self> {
100104
let piconeros_dec =
101105
amount.mul(Decimal::from_u64(PICONERO_OFFSET).expect("constant to fit into u64"));

swap/src/protocol/alice/event_loop.rs

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use futures::stream::{FuturesUnordered, StreamExt};
1313
use libp2p::swarm::SwarmEvent;
1414
use libp2p::{PeerId, Swarm};
1515
use rand::rngs::OsRng;
16+
use rust_decimal::Decimal;
1617
use std::collections::HashMap;
1718
use std::convert::Infallible;
1819
use std::sync::Arc;
@@ -278,7 +279,7 @@ where
278279
.context("Failed to get latest rate")?;
279280

280281
Ok(BidQuote {
281-
price: rate.ask(),
282+
price: rate.ask().context("Failed to compute asking price")?,
282283
max_quantity: max_buy,
283284
})
284285
}
@@ -360,8 +361,9 @@ impl FixedRate {
360361
impl Default for FixedRate {
361362
fn default() -> Self {
362363
let ask = bitcoin::Amount::from_btc(Self::RATE).expect("Static value should never fail");
364+
let spread = Decimal::from(0u64);
363365

364-
Self(Rate::new(ask))
366+
Self(Rate::new(ask, spread))
365367
}
366368
}
367369

@@ -373,13 +375,31 @@ impl LatestRate for FixedRate {
373375
}
374376
}
375377

376-
impl LatestRate for kraken::PriceUpdates {
378+
/// Produces [`Rate`]s based on [`PriceUpdate`]s from kraken and a configured
379+
/// spread.
380+
#[derive(Debug)]
381+
pub struct KrakenRate {
382+
ask_spread: Decimal,
383+
price_updates: kraken::PriceUpdates,
384+
}
385+
386+
impl KrakenRate {
387+
pub fn new(ask_spread: Decimal, price_updates: kraken::PriceUpdates) -> Self {
388+
Self {
389+
ask_spread,
390+
price_updates,
391+
}
392+
}
393+
}
394+
395+
impl LatestRate for KrakenRate {
377396
type Error = kraken::Error;
378397

379398
fn latest_rate(&mut self) -> Result<Rate, Self::Error> {
380-
let update = self.latest_update()?;
399+
let update = self.price_updates.latest_update()?;
400+
let rate = Rate::new(update.ask, self.ask_spread);
381401

382-
Ok(Rate::new(update.ask))
402+
Ok(rate)
383403
}
384404
}
385405

0 commit comments

Comments
 (0)