Skip to content

Commit

Permalink
feat: Allow resizing positions
Browse files Browse the repository at this point in the history
TODO:

- Reintroduce resizing e2e tests.
- Check behaviour across versions i.e. before this patch.

Limitations:

- We rely on having a filling order to be able to identify the
resizing protocol. I think this still is not a resilient approach. I
left a TODO that we probably want to tackle before merging.
  • Loading branch information
luckysori committed Apr 5, 2024
1 parent 39417b8 commit b7d2e15
Show file tree
Hide file tree
Showing 9 changed files with 1,069 additions and 79 deletions.
66 changes: 60 additions & 6 deletions coordinator/src/db/positions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@ use anyhow::ensure;
use anyhow::Result;
use bitcoin::secp256k1::PublicKey;
use bitcoin::Amount;
use bitcoin::SignedAmount;
use diesel::prelude::*;
use diesel::query_builder::QueryId;
use diesel::result::QueryResult;
use diesel::AsExpression;
use diesel::FromSqlRow;
use dlc_manager::ContractId;
use hex::FromHex;
use rust_decimal::prelude::ToPrimitive;
use rust_decimal::Decimal;
use std::any::TypeId;
use time::OffsetDateTime;

Expand Down Expand Up @@ -133,18 +136,27 @@ impl Position {
Ok(positions)
}

/// sets the status of the position in state `Proposed` to a new state
pub fn update_proposed_position(
/// Set the `position_state` column from to `updated`. This will only succeed if the column does
/// match one of the values contained in `original`.
pub fn update_position_state(
conn: &mut PgConnection,
trader_pubkey: String,
state: crate::position::models::PositionState,
original: Vec<crate::position::models::PositionState>,
updated: crate::position::models::PositionState,
) -> QueryResult<crate::position::models::Position> {
let state = PositionState::from(state);
if original.is_empty() {
// It is not really a `NotFound` error, but `diesel` does not make it easy to build
// other variants.
return QueryResult::Err(diesel::result::Error::NotFound);
}

let updated = PositionState::from(updated);

let position: Position = diesel::update(positions::table)
.filter(positions::trader_pubkey.eq(trader_pubkey.clone()))
.filter(positions::position_state.eq(PositionState::Proposed))
.filter(positions::position_state.eq_any(original.into_iter().map(PositionState::from)))
.set((
positions::position_state.eq(state),
positions::position_state.eq(updated),
positions::update_timestamp.eq(OffsetDateTime::now_utc()),
))
.get_result(conn)?;
Expand Down Expand Up @@ -232,6 +244,48 @@ impl Position {
Ok(())
}

#[allow(clippy::too_many_arguments)]
pub fn set_position_to_resizing(
conn: &mut PgConnection,
trader_pubkey: PublicKey,
temporary_contract_id: ContractId,
quantity: Decimal,
trader_direction: trade::Direction,
trader_margin: Amount,
coordinator_margin: Amount,
average_entry_price: Decimal,
expiry: OffsetDateTime,
trader_liquidation_price: Decimal,
// Reducing or changing direction may generate PNL.
realized_pnl: Option<SignedAmount>,
order_matching_fee: Amount,
) -> QueryResult<usize> {
let resize_trader_realized_pnl_sat = realized_pnl.unwrap_or_default().to_sat();

diesel::update(positions::table)
.filter(positions::trader_pubkey.eq(trader_pubkey.to_string()))
.filter(positions::position_state.eq(PositionState::Open))
.set((
positions::position_state.eq(PositionState::Resizing),
positions::temporary_contract_id.eq(hex::encode(temporary_contract_id)),
positions::quantity.eq(quantity.to_f32().expect("to fit")),
positions::trader_direction.eq(Direction::from(trader_direction)),
positions::average_entry_price.eq(average_entry_price.to_f32().expect("to fit")),
positions::trader_liquidation_price
.eq(trader_liquidation_price.to_f32().expect("to fit")),
positions::coordinator_margin.eq(coordinator_margin.to_sat() as i64),
positions::expiry_timestamp.eq(expiry),
positions::trader_realized_pnl_sat
.eq(positions::trader_realized_pnl_sat + resize_trader_realized_pnl_sat),
positions::trader_unrealized_pnl_sat.eq(0),
positions::trader_margin.eq(trader_margin.to_sat() as i64),
positions::update_timestamp.eq(OffsetDateTime::now_utc()),
positions::order_matching_fees
.eq(positions::order_matching_fees + order_matching_fee.to_sat() as i64),
))
.execute(conn)
}

pub fn set_position_to_open(
conn: &mut PgConnection,
trader_pubkey: String,
Expand Down
3 changes: 2 additions & 1 deletion coordinator/src/dlc_protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -443,9 +443,10 @@ impl DlcProtocolExecutor {
// TODO(holzeis): We are still updating the position based on the position state. This
// will change once we only have a single position per user and representing
// the position only as view on multiple trades.
let position = db::positions::Position::update_proposed_position(
let position = db::positions::Position::update_position_state(
conn,
trade_params.trader.to_string(),
vec![PositionState::Proposed, PositionState::Resizing],
PositionState::Open,
)?;

Expand Down
6 changes: 4 additions & 2 deletions coordinator/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -403,9 +403,10 @@ impl Node {
"DLC Channel offer has been rejected. Setting position to failed."
);

db::positions::Position::update_proposed_position(
db::positions::Position::update_position_state(
&mut connection,
node_id.to_string(),
vec![PositionState::Proposed],
PositionState::Failed,
)?;
}
Expand Down Expand Up @@ -439,9 +440,10 @@ impl Node {
"DLC Channel renew offer has been rejected. Setting position to failed."
);

db::positions::Position::update_proposed_position(
db::positions::Position::update_position_state(
&mut connection,
node_id.to_string(),
vec![PositionState::Proposed],
PositionState::Failed,
)?;
}
Expand Down
2 changes: 2 additions & 0 deletions coordinator/src/payout_curve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ pub fn build_contract_descriptor(
/// Additionally returns the [`RoundingIntervals`] to indicate how it should be discretized.
#[allow(clippy::too_many_arguments)]
fn build_inverse_payout_function(
// TODO: The `coordinator_margin` and `trader_margin` are _not_ orthogonal to the other
// arguments passed in.
coordinator_margin: u64,
trader_margin: u64,
initial_price: Decimal,
Expand Down
62 changes: 43 additions & 19 deletions coordinator/src/position/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -433,27 +433,51 @@ impl std::fmt::Debug for NewPosition {

impl std::fmt::Debug for Position {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self {
id,
trader,
contract_symbol,
quantity,
trader_direction,
average_entry_price,
closing_price,
trader_realized_pnl_sat,
trader_liquidation_price,
trader_margin,
coordinator_margin,
trader_leverage,
coordinator_leverage,
position_state,
order_matching_fees,
creation_timestamp,
expiry_timestamp,
update_timestamp,
temporary_contract_id,
stable,
} = self;

f.debug_struct("Position")
.field("id", &self.id)
.field("contract_symbol", &self.contract_symbol)
.field("trader_leverage", &self.trader_leverage)
.field("quantity", &self.quantity)
.field("trader_direction", &self.trader_direction)
.field("average_entry_price", &self.average_entry_price)
.field("trader_liquidation_price", &self.trader_liquidation_price)
.field("position_state", &self.position_state)
.field("coordinator_margin", &self.coordinator_margin)
.field("creation_timestamp", &self.creation_timestamp)
.field("expiry_timestamp", &self.expiry_timestamp)
.field("update_timestamp", &self.update_timestamp)
.field("id", &id)
.field("contract_symbol", &contract_symbol)
.field("trader_leverage", &trader_leverage)
.field("quantity", &quantity)
.field("trader_direction", &trader_direction)
.field("average_entry_price", &average_entry_price)
.field("trader_liquidation_price", &trader_liquidation_price)
.field("position_state", &position_state)
.field("coordinator_margin", &coordinator_margin)
.field("creation_timestamp", &creation_timestamp)
.field("expiry_timestamp", &expiry_timestamp)
.field("update_timestamp", &update_timestamp)
// Otherwise we end up printing the hex of the internal representation.
.field("trader", &self.trader.to_string())
.field("coordinator_leverage", &self.coordinator_leverage)
.field("temporary_contract_id", &self.temporary_contract_id)
.field("closing_price", &self.closing_price)
.field("trader_margin", &self.trader_margin)
.field("stable", &self.stable)
.field("trader_realized_pnl_sat", &self.trader_realized_pnl_sat)
.field("trader", &trader.to_string())
.field("coordinator_leverage", &coordinator_leverage)
.field("temporary_contract_id", &temporary_contract_id)
.field("closing_price", &closing_price)
.field("trader_margin", &trader_margin)
.field("stable", &stable)
.field("trader_realized_pnl_sat", &trader_realized_pnl_sat)
.field("order_matching_fees", &order_matching_fees)
.finish()
}
}
Expand Down
Loading

0 comments on commit b7d2e15

Please sign in to comment.