Skip to content

Commit

Permalink
fix: create trade after finished dlc protocol
Browse files Browse the repository at this point in the history
This patch introduces the contract transaction, which reflects the state of the dlc protocol.

A contract transaction starts with one of the following dlc protocols

- offer channel
- offer collaborative settle
- renew offer
- offer collaborative close
- force closure

The contract txid is used as a reference id for the offer channel, offer collaborative settle and renew offer protocols. Once the final message of the protocol is received, the contract transaction gets updated with the latest dlc ids and creates the corresponding trade.

```
orderbook=# select id, previous_txid, contract_txid, channel_id, contract_id, transaction_state from contract_transactions;
 id |            previous_txid             |            contract_txid             |                            channel_id                            |                           contract_id                            | transaction_state
----+--------------------------------------+--------------------------------------+------------------------------------------------------------------+------------------------------------------------------------------+-------------------
  1 |                                      | 996ef088-f30c-4b9a-b386-9dd502416777 | bfd333e680af603b52340c4e84335c6b9b2fb950363db8a4c77589d494bc8d1e | eba735ded90d3db6516d8260a1361d69f32da484b1b3b0a8e7612ae42f8768fe | Success
  2 | 996ef088-f30c-4b9a-b386-9dd502416777 | caf1335c-be7f-478e-aadb-e996055a8668 | bfd333e680af603b52340c4e84335c6b9b2fb950363db8a4c77589d494bc8d1e | eba735ded90d3db6516d8260a1361d69f32da484b1b3b0a8e7612ae42f8768fe | Success
  3 | caf1335c-be7f-478e-aadb-e996055a8668 | 06d443f1-17e7-4536-8640-e4ff7ac79b91 | bfd333e680af603b52340c4e84335c6b9b2fb950363db8a4c77589d494bc8d1e | 402ac89527d6aa15c26fc550c21390b5d603e645b37358c3dd8e1c22230de826 | Success
  4 | 06d443f1-17e7-4536-8640-e4ff7ac79b91 | 322f3e46-4817-4c57-b8a6-3f641f800988 | bfd333e680af603b52340c4e84335c6b9b2fb950363db8a4c77589d494bc8d1e | 402ac89527d6aa15c26fc550c21390b5d603e645b37358c3dd8e1c22230de826 | Success
  5 | 322f3e46-4817-4c57-b8a6-3f641f800988 | 535303e3-d00f-4a13-9cab-6998b629be42 | bfd333e680af603b52340c4e84335c6b9b2fb950363db8a4c77589d494bc8d1e | eceef4e5a6a3b7c81500f70379a807c66d61468496850b1b06a74884f5be7395 | Success
```

Note, the trade params are temporarily stored to the `trade_params` table so that the trade can be created correctly.
  • Loading branch information
holzeis committed Feb 22, 2024
1 parent 3191380 commit 8a58950
Show file tree
Hide file tree
Showing 19 changed files with 941 additions and 224 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
DROP TABLE if exists trade_params;
DROP TABLE if exists contract_transactions;

DROP TYPE IF EXISTS "Contract_Transactions_State_Type";
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@

CREATE TYPE "Contract_Transactions_State_Type" AS ENUM ('Pending', 'Success', 'Failed');

CREATE TABLE "contract_transactions"
(
id SERIAL PRIMARY KEY NOT NULL,
contract_txid UUID UNIQUE NOT NULL,
previous_txid UUID REFERENCES contract_transactions (contract_txid),
channel_id TEXT NOT NULL,
contract_id TEXT NOT NULL,
transaction_state "Contract_Transactions_State_Type" NOT NULL,
trader_pubkey TEXT NOT NULL REFERENCES users (pubkey),
timestamp timestamp WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE "trade_params"
(
id SERIAL PRIMARY KEY NOT NULL,
contract_txid UUID NOT NULL REFERENCES contract_transactions(contract_txid),
trader_pubkey TEXT NOT NULL,
quantity REAL NOT NULL,
leverage REAL NOT NULL,
average_price REAL NOT NULL,
direction "Direction_Type" NOT NULL
);


212 changes: 212 additions & 0 deletions coordinator/src/contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
use crate::db;
use crate::position::models::PositionState;
use crate::trade::models::NewTrade;
use anyhow::Result;
use bitcoin::secp256k1::PublicKey;
use diesel::r2d2::ConnectionManager;
use diesel::r2d2::Pool;
use diesel::result::Error::RollbackTransaction;
use diesel::Connection;
use diesel::PgConnection;
use dlc_manager::ContractId;
use dlc_manager::ReferenceId;
use ln_dlc_node::node::rust_dlc_manager::DlcChannelId;
use rust_decimal::prelude::FromPrimitive;
use rust_decimal::Decimal;
use time::OffsetDateTime;
use trade::cfd::calculate_margin;
use trade::cfd::calculate_pnl;
use trade::Direction;
use uuid::Uuid;

pub struct ContractTransaction {
pub id: Uuid,
pub timestamp: OffsetDateTime,
pub channel_id: DlcChannelId,
pub contract_id: ContractId,
pub trader: PublicKey,
pub transaction_state: ContractTransactionState,
}

pub struct TradeParams {
pub contract_txid: Uuid,
pub trader: PublicKey,
pub quantity: f32,
pub leverage: f32,
pub average_price: f32,
pub direction: Direction,
}

pub enum ContractTransactionState {
Pending,
Success,
Failed,
}

pub struct ContractExecutor {
pool: Pool<ConnectionManager<PgConnection>>,
}

impl ContractExecutor {
pub fn new(pool: Pool<ConnectionManager<PgConnection>>) -> Self {
ContractExecutor { pool }
}

/// Starts a contract transaction, by creating a new contract transaction and temporarily stores
/// the trade params.
///
/// Returns an uniquely generated contract txid as [`dlc_manager::ReferenceId`]
pub fn start_contract_transaction(
&self,
contract_txid: ReferenceId,
previous_txid: Option<ReferenceId>,
contract_id: ContractId,
channel_id: DlcChannelId,
trade_params: &commons::TradeParams,
) -> Result<()> {
let mut conn = self.pool.get()?;
conn.transaction(|conn| {
db::contract_transactions::create(
conn,
contract_txid,
previous_txid,
contract_id,
channel_id,
&trade_params.pubkey,
)?;
db::trade_params::insert(conn, contract_txid, trade_params)?;

diesel::result::QueryResult::Ok(())
})?;

Ok(())
}

pub fn fail_contract_transaction(&self, contract_txid: ReferenceId) -> Result<()> {
let mut conn = self.pool.get()?;
db::contract_transactions::set_contract_transaction_state_to_failed(&mut conn, contract_txid)?;

Ok(())
}

/// Completes the contract transaction as successful and updates the 10101 meta data
/// accordingly in a single database transaction.
/// - Set contract transaction to success
/// - If not closing: Updates the `[PostionState::Proposed`] position state to
/// `[PostionState::Open]`
/// - If closing: Calculates the pnl and sets the `[PostionState::Closing`] position state to
/// `[PostionState::Closed`]
/// - Creates and inserts the new trade
pub fn finish_contract_transaction(
&self,
contract_txid: ReferenceId,
closing: bool,
contract_id: ContractId,
channel_id: DlcChannelId,
) -> Result<()> {
let mut conn = self.pool.get()?;

conn.transaction(|conn| {
let trade_params: TradeParams = db::trade_params::get(conn, contract_txid)?;

db::contract_transactions::set_contract_transaction_state_to_success(
conn,
contract_txid,
contract_id,
channel_id,
)?;

// 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 = match closing {
false => db::positions::Position::update_proposed_position(
conn,
trade_params.trader.to_string(),
PositionState::Open,
),
true => {
let position = match db::positions::Position::get_position_by_trader(
conn,
trade_params.trader,
vec![
// The price doesn't matter here.
PositionState::Closing { closing_price: 0.0 },
],
)? {
Some(position) => position,
None => {
tracing::error!("No position in state Closing found.");
return Err(RollbackTransaction);
}
};

tracing::debug!(
?position,
trader_id = %trade_params.trader,
"Finalize closing position",
);

let pnl = {
let (initial_margin_long, initial_margin_short) =
match trade_params.direction {
Direction::Long => {
(position.trader_margin, position.coordinator_margin)
}
Direction::Short => {
(position.coordinator_margin, position.trader_margin)
}
};

match calculate_pnl(
Decimal::from_f32(position.average_entry_price)
.expect("to fit into decimal"),
Decimal::from_f32(trade_params.average_price)
.expect("to fit into decimal"),
trade_params.quantity,
trade_params.direction,
initial_margin_long as u64,
initial_margin_short as u64,
) {
Ok(pnl) => pnl,
Err(e) => {
tracing::error!("Failed to calculate pnl. Error: {e:#}");
return Err(RollbackTransaction);
}
}
};

db::positions::Position::set_position_to_closed_with_pnl(conn, position.id, pnl)
}
}?;

let coordinator_margin = calculate_margin(
Decimal::try_from(trade_params.average_price).expect("to fit into decimal"),
trade_params.quantity,
crate::trade::coordinator_leverage_for_trade(&trade_params.trader)
.map_err(|_| RollbackTransaction)?,
);

// TODO(holzeis): Add optional pnl to trade.
// Instead of tracking pnl on the position we want to track pnl on the trade. e.g. Long
// -> Short or Short -> Long.
let new_trade = NewTrade {
position_id: position.id,
contract_symbol: position.contract_symbol,
trader_pubkey: trade_params.trader,
quantity: trade_params.quantity,
trader_leverage: trade_params.leverage,
coordinator_margin: coordinator_margin as i64,
trader_direction: trade_params.direction,
average_price: trade_params.average_price,
dlc_expiry_timestamp: None,
};

db::trades::insert(conn, new_trade)?;

db::trade_params::delete(conn, contract_txid)
})?;

Ok(())
}
}

0 comments on commit 8a58950

Please sign in to comment.