Skip to content

Commit

Permalink
refactor math lib
Browse files Browse the repository at this point in the history
  • Loading branch information
kinrezC committed Oct 10, 2023
1 parent 5af9a3b commit 415db02
Show file tree
Hide file tree
Showing 9 changed files with 254 additions and 45 deletions.
25 changes: 13 additions & 12 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 Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ members = ["box-core", "box-simulation"]

[workspace.dependencies]
bindings = { path = "./box-contracts/out/bindings", version = "0.1.0" }
box_core = { path = "./box-core", version = "0.1.0" }
2 changes: 1 addition & 1 deletion box-core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[package]
name = "box-core"
name = "box_core"
version = "0.1.0"
edition = "2021"

Expand Down
106 changes: 76 additions & 30 deletions box-core/src/math.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,55 @@ pub fn compute_returns(values: Vec<f64>) -> Vec<f64> {
returns
}

/// Computes the mean of a series of values (arithmetic average), finds the mean
/// of the summed differences of the values vs. the mean.
pub fn compute_variance(values: Vec<f64>) -> f64 {
let mean = values.iter().sum::<f64>() / values.len() as f64;
let variance = values
.iter()
.map(|&return_| (return_ - mean).powi(2))
.sum::<f64>()
/ values.len() as f64;
variance
pub trait ComputeReturns {
fn compute_log_returns(self) -> Vec<f64>;
fn compute_variance(self) -> f64;
fn compute_std_deviation(self) -> f64;
fn compute_realized_volatility(self) -> f64;
fn compute_sharpe_ratio(self) -> f64;
}

/// Computes the square root of the variance.
pub fn compute_std_deviation(values: Vec<f64>) -> f64 {
let variance = compute_variance(values);
variance.sqrt()
}

/// The sharpe ratio is the ratio of the mean of the returns to the standard
/// deviation of the returns.
pub fn compute_sharpe_ratio(values: Vec<f64>) -> f64 {
let returns = compute_returns(values);
let mean = returns.iter().sum::<f64>() / returns.len() as f64;
let std_deviation = compute_std_deviation(returns);
mean / std_deviation
impl<I> ComputeReturns for I
where
I: IntoIterator<Item = f64>,
{
fn compute_log_returns(self) -> Vec<f64> {
let mut previous_value = 0.0_f64;
let mut returns = Vec::new();
for value in self {
if previous_value != 0.0 {
returns.push((value / previous_value).ln());
}
previous_value = value;
}
returns
}
fn compute_variance(self) -> f64 {
let values = self.into_iter().collect::<Vec<f64>>();
let mean = values.iter().sum::<f64>() / values.len() as f64;
let variance = values
.iter()
.map(|&return_| (return_ - mean).powi(2))
.sum::<f64>()
/ values.len() as f64;
variance
}
fn compute_std_deviation(self) -> f64 {
let variance = self.compute_variance();
variance.sqrt()
}
fn compute_realized_volatility(self) -> f64 {
let returns = self.compute_log_returns();
let len = returns.len() + 1;
let rv = returns.compute_std_deviation() / (len as f64 / 365.0);
rv
}
fn compute_sharpe_ratio(self) -> f64 {
let returns = self.compute_log_returns();
let mean = returns.iter().sum::<f64>() / returns.len() as f64;
let std_deviation = returns.compute_std_deviation();
mean / std_deviation
}
}

/// Finds the volatility delta to match the target volatility, with respect to
Expand All @@ -57,30 +81,52 @@ pub fn compute_target_volatility_delta(
// todo: make sure these tests are correct, used copilot for them.
mod tests {
#[test]
fn test_compute_returns() {
fn test_compute_log_returns() {
use super::ComputeReturns;
let values = vec![1.0, 2.0, 3.0];
let returns = super::compute_returns(values);
assert_eq!(returns, vec![1.0, 0.5]);
let returns = values.compute_log_returns();
//let returns = super::compute_returns(values);
//assert_eq!(returns, vec![1.0, 0.5]);
assert_eq!(returns, [0.6931471805599453, 0.4054651081081644]);
}

#[test]
fn test_compute_variance() {
use super::ComputeReturns;
let values = vec![1.0, 2.0, 3.0];
let variance = super::compute_variance(values);
let variance = values.compute_variance();
assert_eq!(variance, 0.6666666666666666);
}

#[test]
fn test_compute_std_deviation() {
use super::ComputeReturns;
let values = vec![1.0, 2.0, 3.0];
let std_deviation = super::compute_std_deviation(values);
let std_deviation = values.compute_std_deviation();
assert_eq!(std_deviation, 0.816496580927726);
}

#[test]
fn test_std_dev_log_returns() {
use super::ComputeReturns;
let values = vec![1.0, 2.0, 3.0];
let vol = values.compute_log_returns().compute_std_deviation() / (3.0 / 365.0);
assert_eq!(vol, 17.50065940748334);
}

#[test]
fn test_realized_volatility() {
use super::ComputeReturns;
let values = vec![1.0, 2.0, 3.0];
let vol = values.compute_realized_volatility();
assert_eq!(vol, 17.50065940748334);
}

#[test]
fn test_compute_sharpe_ratio() {
use super::ComputeReturns;
let values = vec![1.0, 2.0, 3.0];
let sharpe_ratio = super::compute_sharpe_ratio(values);
assert_eq!(sharpe_ratio, 3.0);
let sharpe_ratio = values.compute_sharpe_ratio();
assert_eq!(sharpe_ratio, 3.8188416793064177);
}
}
1 change: 1 addition & 0 deletions box-simulation/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ edition = "2021"

[dependencies]
bindings.workspace = true
box_core.workspace = true
tokio = { version = "1.11.0", features = ["full"] }
anyhow = "1.0.75"
arbiter-core = { git = "https://github.com/primitivefinance/arbiter.git", branch = "arbiter/version" }
Expand Down
1 change: 1 addition & 0 deletions box-simulation/src/agents/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use super::*;
pub mod arbitrageur;
pub mod atomic_arbitrage;
pub mod rebalancer;
158 changes: 158 additions & 0 deletions box-simulation/src/agents/rebalancer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
use box_core::math::ComputeReturns;
use ethers::utils::format_ether;
use std::collections::VecDeque;
use std::ops::Div;

use super::*;

pub struct Rebalancer {
pub lex: LiquidExchange<RevmMiddleware>,
pub g3m: G3M<RevmMiddleware>,
pub next_update_timestamp: u64,
pub target_volatility: f64,
pub asset_vol: U256,
pub portfolio_vol: U256,
pub last_asset_value: f64,
pub last_portfolio_value: f64,
pub portfolio_prices: Vec<f64>,
pub asset_prices: Vec<f64>,
pub portfolio_returns: VecDeque<f64>,
pub asset_returns: VecDeque<f64>,
}

impl Rebalancer {
pub async fn new(
label: &str,
environment: &Environment,
liquid_exchange_address: Address,
exchange_address: Address,
target_volatility: f64,
) -> Result<Self> {
let client = RevmMiddleware::new(environment, Some(label))?;
let lex = LiquidExchange::new(liquid_exchange_address, client.clone());
let g3m = G3M::new(exchange_address, client.clone());

Ok(Self {
lex,
g3m,
target_volatility,
next_update_timestamp: 0,
asset_vol: U256::from(0),
portfolio_vol: U256::from(0),
last_asset_value: 0.0,
last_portfolio_value: 0.0,
portfolio_prices: Vec::new(),
asset_prices: Vec::new(),
portfolio_returns: VecDeque::new(),
asset_returns: VecDeque::new(),
})
}

pub async fn step(&mut self, timestamp: u64) -> Result<()> {
if timestamp > self.next_update_timestamp {
self.next_update_timestamp = timestamp + 86400;
let asset_price = self.lex.price().call().await?;

let reserve_x = self.g3m.reserve_x().call().await?;
let reserve_y = self.g3m.reserve_y().call().await?;
let liquidity = self.g3m.total_liquidity().call().await?;
let x_per_liquidity = reserve_x.div(liquidity);
let y_per_liquidity = reserve_y.div(liquidity);

let portfolio_price = x_per_liquidity
.checked_mul(asset_price)
.unwrap()
.checked_add(y_per_liquidity)
.unwrap();

let asset_price_float = format_ether(asset_price).parse::<f64>().unwrap();
let portfolio_price_float = format_ether(portfolio_price).parse::<f64>().unwrap();

self.append_asset_return(asset_price_float)?;
self.append_portfolio_return(portfolio_price_float)?;
}
Ok(())
}

pub fn append_asset_return(&mut self, asset_price: f64) -> Result<()> {
if self.asset_returns.len() == 15 {
self.asset_returns.pop_front();
}
if self.last_asset_value == 0.0 {
self.asset_returns.push_back(0.0);
} else {
}
self.asset_returns.push_back(new_asset_return);

Ok(())
}

pub fn append_portfolio_return(&mut self, portfolio_price: f64) -> Result<()> {
if self.portfolio_returns.len() == 15 {
self.portfolio_returns.pop_front();
}
self.portfolio_returns.push_back(new_portfolio_return);

Ok(())
}

/// Detects if there is an arbitrage opportunity.
/// Returns the direction of the swap `XtoY` or `YtoX` if there is an
/// arbitrage opportunity. Returns `None` if there is no arbitrage
/// opportunity.
async fn detect_arbitrage(&mut self) -> Result<Swap> {
// Update the prices the for the arbitrageur.
let liquid_exchange_price_wad = self.liquid_exchange.price().call().await?;
let g3m_price_wad = self.g3m.get_spot_price().call().await?;

let gamma_wad = WAD - self.g3m.swap_fee().call().await?;

// Compute the no-arbitrage bounds.
let upper_arb_bound = WAD * g3m_price_wad / gamma_wad;
let lower_arb_bound = g3m_price_wad * gamma_wad / WAD;

// Check if we have an arbitrage opportunity by comparing against the bounds and
// current price.
// If these conditions are not satisfied, there cannot be a profitable
// arbitrage. See: [An Analysis of Uniswap Markets](https://arxiv.org/pdf/1911.03380.pdf) Eq. 3, for example.
if liquid_exchange_price_wad > upper_arb_bound && liquid_exchange_price_wad > g3m_price_wad
{
// Raise the portfolio price by selling asset for quote
Ok(Swap::RaiseExchangePrice(liquid_exchange_price_wad))
} else if liquid_exchange_price_wad < lower_arb_bound
&& liquid_exchange_price_wad < g3m_price_wad
{
// Lower the exchange price by selling asset for quote
Ok(Swap::LowerExchangePrice(liquid_exchange_price_wad))
} else {
// Prices are within the no-arbitrage bounds, so we don't have an arbitrage.
Ok(Swap::None)
}
}

async fn get_x_input(&mut self, target_price_wad: U256) -> Result<U256> {
let weight_x = self.g3m.weight_x().call().await?;
let weight_y = self.g3m.weight_y().call().await?;
let reserve_y = self.g3m.reserve_y().call().await?;
let invariant = self.g3m.get_invariant().call().await?;

Ok(weight_y
* U256::from(1)
.div(target_price_wad * invariant.pow(U256::from(1).div(weight_x)))
.pow(U256::from(1) + weight_y.div(weight_x))
- reserve_y)
}

async fn get_y_input(&mut self, target_price_wad: U256) -> Result<U256> {
let weight_x = self.g3m.weight_x().call().await?;
let weight_y = self.g3m.weight_y().call().await?;
let reserve_x = self.g3m.reserve_x().call().await?;
let invariant = self.g3m.get_invariant().call().await?;

Ok(weight_x
* target_price_wad
.div(invariant.pow(U256::from(1).div(weight_y)))
.pow(U256::from(1) + weight_x.div(weight_y))
- reserve_x)
}
}
2 changes: 2 additions & 0 deletions box-simulation/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,7 @@ async fn main() -> Result<()> {

setup::init::init(&contracts, &config).await;

let init = setup::init::init(&contracts).await?;

Ok(())
}
3 changes: 1 addition & 2 deletions box-simulation/src/setup/deploy.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use anyhow::Result;
use arbiter_core::bindings::arbiter_token::ArbiterToken;
use arbiter_core::bindings::liquid_exchange::LiquidExchange;
use arbiter_core::bindings::{arbiter_token::ArbiterToken, liquid_exchange::LiquidExchange};
use arbiter_core::data_collection::EventLogger;
use arbiter_core::environment::Environment;
use arbiter_core::middleware::RevmMiddleware;
Expand Down

0 comments on commit 415db02

Please sign in to comment.