Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

linear liquidity strategies in pcli #4800

Merged
merged 1 commit into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
297 changes: 56 additions & 241 deletions crates/bin/pcli/src/command/tx/replicate.rs
Original file line number Diff line number Diff line change
@@ -1,293 +1,108 @@
use std::io::Write;
use std::path::PathBuf;
use crate::App;

use anyhow::{anyhow, bail, Context, Result};
use dialoguer::Confirm;
use rand_core::OsRng;
pub mod linear;
pub mod xyk;

use penumbra_asset::Value;
use penumbra_dex::{lp::position::Position, DirectedUnitPair};
use penumbra_keys::keys::AddressIndex;
use penumbra_num::{fixpoint::U128x128, Amount};
use penumbra_proto::{
core::component::dex::v1::{
query_service_client::QueryServiceClient as DexQueryServiceClient, SpreadRequest,
},
view::v1::GasPricesRequest,
use linear::Linear;
use penumbra_dex::DirectedUnitPair;
use penumbra_proto::core::component::dex::v1::{
query_service_client::QueryServiceClient as DexQueryServiceClient, SpreadRequest,
};
use penumbra_view::{Planner, ViewClient};

use crate::dex_utils;
use crate::dex_utils::replicate::debug;
use crate::{warning, App};
use xyk::ConstantProduct;

/// Queries the chain for a transaction by hash.
#[derive(Debug, clap::Subcommand)]
pub enum ReplicateCmd {
#[clap(visible_alias = "xyk")]
/// Create a set of positions that attempt to replicate an xy=k (UniV2) AMM.
// Hidden pending further testing & review
#[clap(visible_alias = "xyk", hide = true)]
ConstantProduct(ConstantProduct),
/// Create a set of positions that allocate liquidity linearly across a price range.
Linear(Linear),
}

impl ReplicateCmd {
pub async fn exec(&self, app: &mut App) -> anyhow::Result<()> {
match self {
ReplicateCmd::ConstantProduct(xyk_cmd) => xyk_cmd.exec(app).await?,
ReplicateCmd::Linear(linear_cmd) => linear_cmd.exec(app).await?,
};
Ok(())
}

pub fn offline(&self) -> bool {
match self {
ReplicateCmd::ConstantProduct(_) => false,
}
false
}
}

#[derive(Debug, Clone, clap::Args)]
pub struct ConstantProduct {
pub pair: DirectedUnitPair,
pub input: Value,

#[clap(short, long)]
pub current_price: Option<f64>,

#[clap(short, long, default_value_t = 0u32)]
pub fee_bps: u32,
/// `--yes` means all prompt interaction are skipped and agreed.
#[clap(short, long)]
pub yes: bool,

#[clap(short, long, hide(true))]
pub debug_file: Option<PathBuf>,
#[clap(long, default_value = "0", hide(true))]
pub source: u32,
}

impl ConstantProduct {
pub async fn exec(&self, app: &mut App) -> anyhow::Result<()> {
self.validate()?;
let pair = self.pair.clone();
let current_price = match self.current_price {
Some(user_supplied_price) => user_supplied_price,
None => self.get_spread(app).await?,
};

let positions = dex_utils::replicate::xyk::replicate(
&pair,
&self.input,
current_price.try_into()?,
self.fee_bps,
)?;

let (amount_start, amount_end) =
positions
.iter()
.fold((Amount::zero(), Amount::zero()), |acc, pos| {
(
acc.0
+ pos
.reserves_for(pair.start.id())
.expect("start is part of position"),
acc.1
+ pos
.reserves_for(pair.end.id())
.expect("end is part of position"),
)
});
let amount_start = pair.start.format_value(amount_start);
let amount_end = pair.end.format_value(amount_end);

warning::rmm();
pub async fn process_price_or_fetch_spread(
app: &mut App,
user_price: Option<f64>,
directed_pair: DirectedUnitPair,
) -> anyhow::Result<f64> {
let canonical_pair = directed_pair.into_directed_trading_pair().to_canonical();

if !self.yes
&& !Confirm::new()
.with_prompt("In the solemn voice of Mandos, he who sets the fates of all, you hear a question,\nechoing like a whisper through the Halls of Waiting:\n\"Do you, in your heart of hearts, truly wish to proceed?\"")
.interact()?
{
return Ok(());
}
println!("\nso it shall be...\n\n");
println!(
"#################################################################################"
);
println!(
"########################### LIQUIDITY SUMMARY ###################################"
);
println!(
"#################################################################################"
);
println!("\nYou want to provide liquidity on the pair {}", pair);
println!("You will need:",);
println!(" -> {amount_start}{}", pair.start);
println!(" -> {amount_end}{}", pair.end);
// TODO(erwan): would be nice to print current balance?

println!("You will create the following pools:");
let asset_cache = app.view().assets().await?;
println!(
"{}",
crate::command::utils::render_positions(&asset_cache, &positions),
);
if let Some(user_price) = user_price {
let adjusted = adjust_price_by_exponents(user_price, &directed_pair);
tracing::debug!(?user_price, ?adjusted, "adjusted price by units");
return Ok(adjusted);
} else {
tracing::debug!("price not provided, fetching spread");

if let Some(debug_file) = &self.debug_file {
Self::write_debug_data(
debug_file.clone(),
self.pair.clone(),
self.input.clone(),
current_price,
positions.clone(),
)?;
return Ok(());
}

if !self.yes
&& !Confirm::new()
.with_prompt("Do you want to open those liquidity positions on-chain?")
.interact()?
{
return Ok(());
}

let gas_prices = app
.view
.as_mut()
.context("view service must be initialized")?
.gas_prices(GasPricesRequest {})
.await?
.into_inner()
.gas_prices
.expect("gas prices must be available")
.try_into()?;

let mut planner = Planner::new(OsRng);
planner.set_gas_prices(gas_prices);
positions.iter().for_each(|position| {
planner.position_open(position.clone());
});

let plan = planner
.plan(
app.view
.as_mut()
.context("view service must be initialized")?,
AddressIndex::new(self.source),
)
.await?;
let tx_id = app.build_and_submit_transaction(plan).await?;
println!("posted with transaction id: {tx_id}");

Ok(())
}

fn validate(&self) -> anyhow::Result<()> {
if self.input.asset_id != self.pair.start.id() && self.input.asset_id != self.pair.end.id()
{
anyhow::bail!("you must supply liquidity with an asset that's part of the market")
} else if self.input.amount == 0u64.into() {
anyhow::bail!("the quantity of liquidity supplied must be non-zero.",)
} else if self.fee_bps > 5000 {
anyhow::bail!("the maximum fee is 5000bps (50%)")
} else if self.current_price.is_some()
&& self.current_price.expect("current price is Some") <= 0.0
{
anyhow::bail!("the supplied current price must be positive")
} else {
Ok(())
}
}

async fn get_spread(&self, app: &mut App) -> Result<f64> {
let mut client = DexQueryServiceClient::new(app.pd_channel().await?);
let spread_data = client
.spread(SpreadRequest {
trading_pair: Some(self.pair.into_directed_trading_pair().to_canonical().into()),
trading_pair: Some(canonical_pair.into()),
})
.await?
.into_inner();

tracing::debug!(
?spread_data,
pair = self.pair.to_string(),
pair = canonical_pair.to_string(),
"fetched spread for pair"
);

if spread_data.best_1_to_2_position.is_none() || spread_data.best_2_to_1_position.is_none()
{
bail!("couldn't find a market price for the specified assets, you can manually specify a price using --current-price <price>")
anyhow::bail!("couldn't find a market price for the specified assets, you can manually specify a price using --current-price <price>")
}

if self.input.asset_id == self.pair.start.id() {
Ok(spread_data.approx_effective_price_1_to_2)
} else if self.input.asset_id == self.pair.end.id() {
// The price is the amount of asset 2 required to buy 1 unit of asset 1

if directed_pair.start.id() == canonical_pair.asset_1() {
Ok(spread_data.approx_effective_price_2_to_1)
} else if directed_pair.start.id() == canonical_pair.asset_2() {
Ok(spread_data.approx_effective_price_1_to_2)
} else {
bail!("the supplied liquidity must be on the pair")
anyhow::bail!("the supplied liquidity must be on the pair")
}
}
}

pub(crate) fn write_debug_data(
file: PathBuf,
pair: DirectedUnitPair,
input: Value,
current_price: f64,
positions: Vec<Position>,
) -> anyhow::Result<()> {
// Ad-hoc denom scaling for debug data:
let alphas = dex_utils::replicate::xyk::sample_prices(
current_price,
dex_utils::replicate::xyk::NUM_POOLS_PRECISION,
);

alphas
.iter()
.enumerate()
.for_each(|(i, alpha)| tracing::debug!(i, alpha, "sampled tick"));

let r1: f64;

{
let raw_r1 = input.amount.value();
let denom_unit = pair.start.unit_amount().value();
let fp_r1 = U128x128::ratio(raw_r1, denom_unit).expect("denom unit is not 0");
r1 = fp_r1.into();
}
fn adjust_price_by_exponents(price: f64, pair: &DirectedUnitPair) -> f64 {
let start_exponent = pair.start.exponent() as i32;
let end_exponent = pair.end.exponent() as i32;

let r2 = r1 * current_price;
let total_k = r1 * r2;
println!("Entry R1: {r1}");
println!("Entry R2: {r2}");
println!("total K: {total_k}");
price * 10f64.powi(start_exponent - end_exponent)
}

let debug_positions: Vec<debug::PayoffPositionEntry> = positions
.iter()
.zip(alphas)
.enumerate()
.map(|(idx, (pos, alpha))| {
let payoff_entry = debug::PayoffPosition::from_position(pair.clone(), pos.clone());
debug::PayoffPositionEntry {
payoff: payoff_entry,
current_price,
index: idx,
pair: pair.clone(),
alpha,
total_k,
}
})
.collect();
#[cfg(test)]
mod tests {
use super::*;

let mut fd = std::fs::File::create(&file).map_err(|e| {
anyhow!(
"fs error opening debug file {}: {}",
file.to_string_lossy(),
e
)
})?;
#[test]
fn test_adjust_price_by_exponents() {
let pair1: DirectedUnitPair = "penumbra:gm".parse().unwrap();
let pair2: DirectedUnitPair = "upenumbra:gm".parse().unwrap();
let pair3: DirectedUnitPair = "penumbra:ugm".parse().unwrap();
let pair4: DirectedUnitPair = "upenumbra:ugm".parse().unwrap();

let json_data = serde_json::to_string(&debug_positions)
.map_err(|e| anyhow!("error serializing PayoffPositionEntry: {}", e))?;
let base_price = 1.2;

fd.write_all(json_data.as_bytes())
.map_err(|e| anyhow!("error writing {}: {}", file.to_string_lossy(), e))?;
Ok(())
assert_eq!(adjust_price_by_exponents(base_price, &pair1), 1.2);
assert_eq!(adjust_price_by_exponents(base_price, &pair2), 0.0000012);
assert_eq!(adjust_price_by_exponents(base_price, &pair3), 1200000.0);
assert_eq!(adjust_price_by_exponents(base_price, &pair4), 1.2);
}
}
Loading
Loading