Skip to content

Commit

Permalink
feat: Generate own step payout function
Browse files Browse the repository at this point in the history
Before this patch, we generated a polynomial payout function which
would then be transformed by `rust-dlc` into a step function. This
approach was problematic because we could not trust `rust-dlc` to not
mess with the intervals excessively.

Specifically, any changes to the liquidation intervals could cause
problems because they represent the collateral reserves, which are
used to indicate available balances and are circumstantially needed to
complete features such as position resizing.

Now we take full control of the payout function generation, directly
generating a step function that can be consumed by `rust-dlc`
_without_ rounding. The payout is calculated by taking the PNL at the
half-way point of the interval.

It is important to emphasise that we must opt out
of rounding by using a `RoundingIntervals` with a single element of
the form:

```rust
RoundingInterval {
    begin_interval: 0,
    rounding_mod: 1,
}
```

Overall, I think the code is considerably simpler and there are fewer
edge cases to handle.

One important thing to consider is that we can now directly control
the number of payouts we generate, which has an effect on the number
of CETs that will be created. This is currently set by the constant
`PAYOUT_CURVE_DISCRETIZATION_INTERVALS`, but in the future we could
make it dynamic.

Additionally, at the moment the majority of the intervals are of equal
length in terms of price. Eventually we might want to generate more,
smaller intervals around the starting price, and make the less likely
intervals bigger as we approach the liquidation zones.

Other notable changes
---------------------

Since we had to replace a snapshot, we've introduced a dev-dependency
to `insta`[^1], a library that makes it easy to write and maintain
snapshot tests. There is a companion tool, `cargo-insta`[^2], which
can help with this process.

[^1]: https://github.com/mitsuhiko/insta.
[^2]: https://github.com/mitsuhiko/insta/tree/master/cargo-insta.
  • Loading branch information
luckysori committed Apr 5, 2024
1 parent 2665f22 commit d73e17f
Show file tree
Hide file tree
Showing 9 changed files with 5,107 additions and 40,432 deletions.
44 changes: 44 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions coordinator/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,6 @@ url = "2.3.1"
uuid = { version = "1.3.0", features = ["v4", "serde"] }

[dev-dependencies]
proptest = "1"
rust_decimal_macros = "1"
testcontainers = "0.14.0"
179 changes: 97 additions & 82 deletions coordinator/src/payout_curve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,11 @@ use dlc_manager::payout_curve::PayoutPoint;
use dlc_manager::payout_curve::PolynomialPayoutCurvePiece;
use dlc_manager::payout_curve::RoundingInterval;
use dlc_manager::payout_curve::RoundingIntervals;
use payout_curve::ROUNDING_PERCENT;
use rust_decimal::prelude::FromPrimitive;
use rust_decimal::Decimal;
use tracing::instrument;
use trade::cfd::calculate_long_liquidation_price;
use trade::cfd::calculate_short_liquidation_price;
use trade::cfd::BTCUSD_MAX_PRICE;
use trade::ContractSymbol;
use trade::Direction;

Expand Down Expand Up @@ -123,18 +121,6 @@ fn build_inverse_payout_function(
coordinator_direction,
)?;

// The payout curve generation code tends to shift the liquidation prices slightly.
let adjusted_long_liquidation_price = payout_points
.first()
.context("Empty payout points")?
.1
.event_outcome;
let adjusted_short_liquidation_price = payout_points
.last()
.context("Empty payout points")?
.0
.event_outcome;

let mut pieces = vec![];
for (lower, upper) in payout_points {
let lower_range = PolynomialPayoutCurvePiece::new(vec![
Expand All @@ -155,14 +141,12 @@ fn build_inverse_payout_function(
let payout_function =
PayoutFunction::new(pieces).context("could not create payout function")?;

let rounding_intervals = {
let total_margin = coordinator_margin + trader_margin;

create_rounding_intervals(
total_margin,
adjusted_long_liquidation_price,
adjusted_short_liquidation_price,
)
let rounding_intervals = RoundingIntervals {
intervals: vec![RoundingInterval {
begin_interval: 0,
// No rounding needed because we are giving `rust-dlc` a step function already.
rounding_mod: 1,
}],
};

Ok((payout_function, rounding_intervals))
Expand All @@ -188,58 +172,11 @@ fn get_liquidation_prices(
(coordinator_liquidation_price, trader_liquidation_price)
}

pub fn create_rounding_intervals(
total_margin: u64,
long_liquidation_price: u64,
short_liquidation_price: u64,
) -> RoundingIntervals {
let liquidation_diff = short_liquidation_price
.checked_sub(long_liquidation_price)
.expect("short liquidation to be higher than long liquidation");
let low_price = long_liquidation_price + liquidation_diff / 10;
let high_price = short_liquidation_price - liquidation_diff / 10;

let mut intervals = vec![
RoundingInterval {
begin_interval: 0,
// No rounding.
rounding_mod: 1,
},
// HACK: We decrease the rounding here to prevent `rust-dlc` from rounding under the long
// liquidation price _payout_.
RoundingInterval {
begin_interval: long_liquidation_price,
rounding_mod: (total_margin as f32 * ROUNDING_PERCENT * 0.1) as u64,
},
RoundingInterval {
begin_interval: low_price,
rounding_mod: (total_margin as f32 * ROUNDING_PERCENT) as u64,
},
];

if short_liquidation_price < BTCUSD_MAX_PRICE {
intervals.push(
// HACK: We decrease the rounding here to prevent `rust-dlc` from rounding over the
// short liquidation price _payout_.
RoundingInterval {
begin_interval: high_price,
rounding_mod: (total_margin as f32 * ROUNDING_PERCENT * 0.1) as u64,
},
);
intervals.push(RoundingInterval {
begin_interval: short_liquidation_price,
// No rounding.
rounding_mod: 1,
})
}

RoundingIntervals { intervals }
}

#[cfg(test)]
mod tests {
use super::*;
use commons::order_matching_fee_taker;
use proptest::prelude::*;
use rust_decimal_macros::dec;
use trade::cfd::calculate_margin;

Expand Down Expand Up @@ -300,12 +237,8 @@ mod tests {
}
}

// TODO: We can still end up with a payout function that does not respect the liquidation
// payouts we want to define via the `coordinator_collateral_reserve` and the
// `trader_collateral_reserve`.
#[test]
#[should_panic]
fn payout_function_does_not_always_respect_collateral_reserve() {
fn payout_function_respects_collateral_reserve() {
let initial_price = dec!(28_251);
let quantity = 500.0;
let leverage_coordinator = 2.0;
Expand All @@ -319,7 +252,10 @@ mod tests {
let coordinator_collateral_reserve = 2_120_386;
let trader_collateral_reserve = 5_115_076;

let total_collateral = coordinator_margin + trader_margin;
let total_collateral = coordinator_margin
+ trader_margin
+ coordinator_collateral_reserve
+ trader_collateral_reserve;

let symbol = ContractSymbol::BtcUsd;

Expand All @@ -339,11 +275,9 @@ mod tests {

let range_payouts = match descriptor {
ContractDescriptor::Enum(_) => unreachable!(),
ContractDescriptor::Numerical(numerical) => numerical
.get_range_payouts(
total_collateral + coordinator_collateral_reserve + trader_collateral_reserve,
)
.unwrap(),
ContractDescriptor::Numerical(numerical) => {
numerical.get_range_payouts(total_collateral).unwrap()
}
};

let liquidation_payout_offer = range_payouts
Expand All @@ -354,6 +288,87 @@ mod tests {
.offer;

assert_eq!(liquidation_payout_offer, coordinator_collateral_reserve);

let liquidation_payout_accept = range_payouts
.iter()
.min_by(|a, b| a.payout.accept.cmp(&b.payout.accept))
.unwrap()
.payout
.accept;

assert_eq!(liquidation_payout_accept, trader_collateral_reserve);
}

proptest! {
#[test]
fn payout_function_always_respects_reserves(
quantity in 1.0f32..10_000.0,
initial_price in 20_000u32..80_000,
leverage_coordinator in 1u32..5,
leverage_trader in 1u32..5,
is_coordinator_long in proptest::bool::ANY,
collateral_reserve_coordinator in 0u64..1_000_000,
collateral_reserve_trader in 0u64..1_000_000,
) {
let initial_price = Decimal::from(initial_price);
let leverage_coordinator = leverage_coordinator as f32;
let leverage_trader = leverage_trader as f32;

let margin_coordinator = calculate_margin(initial_price, quantity, leverage_coordinator);
let margin_trader = calculate_margin(initial_price, quantity, leverage_trader);

let coordinator_direction = if is_coordinator_long {
Direction::Long
} else {
Direction::Short
};

let total_collateral = margin_coordinator
+ margin_trader
+ collateral_reserve_coordinator
+ collateral_reserve_trader;

let symbol = ContractSymbol::BtcUsd;

let descriptor = build_contract_descriptor(
initial_price,
margin_coordinator,
margin_trader,
leverage_coordinator,
leverage_trader,
coordinator_direction,
collateral_reserve_coordinator,
collateral_reserve_trader,
quantity,
symbol,
)
.unwrap();

let range_payouts = match descriptor {
ContractDescriptor::Enum(_) => unreachable!(),
ContractDescriptor::Numerical(numerical) => numerical
.get_range_payouts(total_collateral)
.unwrap(),
};

let liquidation_payout_offer = range_payouts
.iter()
.min_by(|a, b| a.payout.offer.cmp(&b.payout.offer))
.unwrap()
.payout
.offer;

assert_eq!(liquidation_payout_offer, collateral_reserve_coordinator);

let liquidation_payout_accept = range_payouts
.iter()
.min_by(|a, b| a.payout.accept.cmp(&b.payout.accept))
.unwrap()
.payout
.accept;

assert_eq!(liquidation_payout_accept, collateral_reserve_trader);
}
}

#[test]
Expand Down Expand Up @@ -393,7 +408,7 @@ mod tests {
}

#[test]
fn build_contract_descriptor_dont_panic() {
fn build_contract_descriptor_does_not_panic() {
let initial_price = dec!(36404.5);
let quantity = 20.0;
let leverage_coordinator = 2.0;
Expand Down
1 change: 1 addition & 0 deletions crates/payout_curve/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ trade = { path = "../trade" }
[dev-dependencies]
csv = "1.3.0"
dlc-manager = { version = "0.4.0", features = ["use-serde"] }
insta = "1"
proptest = "1"
rust_decimal_macros = "1"
tracing = "0.1.37"
Expand Down
Loading

0 comments on commit d73e17f

Please sign in to comment.