diff --git a/simulation/configs/momentum/static.toml b/simulation/configs/momentum/static.toml new file mode 100644 index 000000000..004300cb3 --- /dev/null +++ b/simulation/configs/momentum/static.toml @@ -0,0 +1,53 @@ +simulation = "DynamicWeights" +output_directory = "momentum_static" + +[trajectory] +# The type of price process to use. +process = "gbm" +# The number of steps in the process. +num_steps = 100 +# Seed for the price processes +seed = 2 +# The number of distinct paths to use +num_paths = 1 +# The initial price of the asset. +[trajectory.initial_price] +fixed = 1.0 +# The start time of the process. +[trajectory.t_0] +fixed = 0.0 +# The end time of the process. +[trajectory.t_n] +fixed = 1.0 + +[gbm] +# The drift of the process. +[gbm.drift] +fixed = 0.1 +# The volatility of the process. +[gbm.volatility] +fixed = 0.40 + + +[pool] +# The weight for the `token_x` in the pool. +# `weight_y = 1 - weight_x` +weight_x = 0.5 +# The swap fee in BPs. +fee_basis_points = 30 +# Target volatility for the pool +target_volatility = 0.15 + + +[lp] +x_liquidity = 1.0# in ether + +[weight_changer] +target_volatility = 0.15 +#update_frequency = 450 # 1 update per 30 timestep day +#update_frequency = 300 # 1 update per 20 timestep day +update_frequency = 150 # 1 update per 10 timestep day +#update_frequency = 75 # 1 update per 5 timestep day + +[block] +timestep_size = 15 diff --git a/simulation/src/agents/mod.rs b/simulation/src/agents/mod.rs index a330c22ca..5b385794a 100644 --- a/simulation/src/agents/mod.rs +++ b/simulation/src/agents/mod.rs @@ -6,6 +6,7 @@ pub mod liquidity_provider; pub mod price_changer; pub mod token_admin; pub mod weight_changer; +pub mod momentum_strategist; use std::marker::{Send, Sync}; diff --git a/simulation/src/agents/momentum_strategist.rs b/simulation/src/agents/momentum_strategist.rs new file mode 100644 index 000000000..0b2605e5c --- /dev/null +++ b/simulation/src/agents/momentum_strategist.rs @@ -0,0 +1,181 @@ +use super::*; +use crate::math::*; + +#[derive(Clone)] +pub struct MomentumStrategist { + pub client: Arc, + pub lex: LiquidExchange, + pub g3m: G3M, + pub next_update_timestamp: u64, + pub update_frequency: u64, + pub portfolio_prices: Vec<(f64, u64)>, + pub asset_prices: Vec<(f64, u64)>, + pub portfolio_returns: Vec<(f64, u64)>, + pub asset_returns: Vec<(f64, u64)>, +} + +impl MomentumStrategist { + pub async fn new( + environment: &Environment, + config: &SimulationConfig, + liquid_exchange_address: Address, + arbx: Address, + arby: Address, + ) -> Result { + let client = RevmMiddleware::new(environment, "weight_changer".into())?; + + let g3m_args = ( + arbx, + arby, + ethers::utils::parse_ether(config.pool.weight_x)?, + U256::from(config.pool.fee_basis_points), + ); + let g3m = G3M::deploy(client.clone(), g3m_args)?.send().await?; + let lex = LiquidExchange::new(liquid_exchange_address, client.clone()); + Ok(Self { + client, + lex, + g3m, + update_frequency: config.weight_changer.update_frequency, + next_update_timestamp: config.weight_changer.update_frequency, + portfolio_prices: Vec::new(), + asset_prices: Vec::new(), + portfolio_returns: Vec::new(), + asset_returns: Vec::new(), + }) + } + + fn calculate_returns(&mut self) -> Result<()> { + // if self.asset_prices.len() > 15 then only calcualte for the last 15 elements + if self.asset_prices.len() > 15 { + let asset_return = compute_net_returns( + self.asset_prices + .iter() + .skip(self.asset_prices.len() - 15) + .map(|(price, _)| *price) + .collect::>(), + ); + self.asset_returns.push((asset_return, self.next_update_timestamp)); + } + if self.portfolio_prices.len() > 15 { + let portfolio_return = compute_net_returns( + self.portfolio_prices + .iter() + .skip(self.portfolio_prices.len() - 15) + .map(|(price, _)| *price) + .collect::>(), + ); + + self.portfolio_returns + .push((portfolio_return, self.next_update_timestamp)); + } + info!( + "hypothetical percent asset return: {}", + (self.asset_prices.last().unwrap().0 - self.asset_prices.first().unwrap().0) + / self.asset_prices.first().unwrap().0 + ); + info!( + "portfolio percent return: {}", + (self.portfolio_prices.last().unwrap().0 - self.portfolio_prices.first().unwrap().0) + / self.portfolio_prices.first().unwrap().0 + ); + info!( + "initial portfolio price: {}", + self.portfolio_prices.first().unwrap().0 + ); + info!( + "current portfolio price: {}", + self.portfolio_prices.last().unwrap().0 + ); + + Ok(()) + } + + // dumb poc, this just checks if the portfolio rv is greater than the target rv + // then changes weight by 1% over the course of a day depending on if rv is + // greater or less than target + async fn execute_smooth_rebalance(&mut self) -> Result<()> { + if self.portfolio_returns.len() < 2 { + return Ok(()); + } + let portfolio_return = self.portfolio_returns.last().unwrap().0; + info!("portfolio_rv: {}", portfolio_return); + let current_weight_x = self.g3m.weight_x().call().await?; + let current_weight_float = format_ether(current_weight_x).parse::().unwrap(); + info!("current_weight_float: {}", current_weight_float); + if portfolio_return > 0.0 { + let new_weight = current_weight_float + 0.0025; + info!("new weight: {}", new_weight); + self.g3m + .set_weight_x( + parse_ether(new_weight.to_string()).unwrap(), + U256::from(self.next_update_timestamp), + ) + .send() + .await?; + } else { + let new_weight = current_weight_float - 0.0025; + info!("new weight: {}", new_weight); + self.g3m + .set_weight_x( + parse_ether(new_weight.to_string()).unwrap(), + U256::from(self.next_update_timestamp), + ) + .send() + .await?; + } + Ok(()) + } +} + +#[async_trait::async_trait] +impl Agent for MomentumStrategist { + async fn step(&mut self) -> Result<()> { + let timestamp = self.client.get_block_timestamp().await?.as_u64(); + if timestamp >= self.next_update_timestamp { + self.next_update_timestamp = timestamp + self.update_frequency; + let asset_price = format_ether(self.lex.price().call().await?) + .parse::() + .unwrap(); + + let reserve_x = format_ether(self.g3m.reserve_x_without_precision().call().await?) + .parse::() + .unwrap(); + let reserve_y = format_ether(self.g3m.reserve_y_without_precision().call().await?) + .parse::() + .unwrap(); + + let portfolio_price = reserve_x * asset_price + reserve_y; + info!("portfolio_price: {}", portfolio_price); + + self.asset_prices.push((asset_price, timestamp)); + self.portfolio_prices.push((portfolio_price, timestamp)); + // info!("asset_prices: {:?}", self.asset_prices); + // info!("portfolio_prices: {:?}", self.portfolio_prices); + self.calculate_returns()?; + self.execute_smooth_rebalance().await?; + } + Ok(()) + } + + async fn startup(&mut self) -> Result<()> { + let asset_price = format_ether(self.lex.price().call().await?) + .parse::() + .unwrap(); + + let reserve_x = format_ether(self.g3m.reserve_x_without_precision().call().await?) + .parse::() + .unwrap(); + let reserve_y = format_ether(self.g3m.reserve_y_without_precision().call().await?) + .parse::() + .unwrap(); + + let portfolio_price = reserve_x * asset_price + reserve_y; + info!("portfolio_price: {}", portfolio_price); + + self.portfolio_prices.push((portfolio_price, 0)); + self.asset_prices.push((asset_price, 0)); + + Ok(()) + } +} diff --git a/simulation/src/math.rs b/simulation/src/math.rs index ff5ed9a5a..c7f1a8d8f 100644 --- a/simulation/src/math.rs +++ b/simulation/src/math.rs @@ -36,6 +36,12 @@ pub fn compute_simple_returns(values: impl IntoIterator) -> Vec returns } +pub fn compute_net_returns(values: impl IntoIterator) -> f64 { + let values = values.into_iter().collect::>(); + let net_return = values.last().unwrap_or(&0.0) / values.first().unwrap_or(&1.0) - 1.0; + net_return +} + pub fn compute_variance(values: impl IntoIterator) -> f64 { let values = values.into_iter().collect::>(); let mean = values.iter().sum::() / values.len() as f64; @@ -98,6 +104,12 @@ mod tests { assert_eq!(returns, [1.0, 0.5]); } + #[test] + fn test_compute_net_return() { + let returns = compute_net_returns(VALUES); + assert_eq!(returns, 2.0); + } + #[test] fn test_compute_variance() { let variance = compute_variance(VALUES); diff --git a/simulation/src/simulations/mod.rs b/simulation/src/simulations/mod.rs index c50fc800d..916c58984 100644 --- a/simulation/src/simulations/mod.rs +++ b/simulation/src/simulations/mod.rs @@ -11,6 +11,7 @@ use crate::{ pub mod dynamic_weights; pub mod errors; pub mod stable_portfolio; +pub mod momentum; use settings::parameters::Parameterized; use tokio::runtime::Builder; @@ -24,6 +25,7 @@ pub struct Simulation { pub enum SimulationType { DynamicWeights, StablePortfolio, + MomentumStrategy, } impl SimulationType { @@ -31,6 +33,7 @@ impl SimulationType { let simulation = match config.simulation { SimulationType::DynamicWeights => dynamic_weights::setup(config).await?, SimulationType::StablePortfolio => stable_portfolio::setup(config).await?, + SimulationType::MomentumStrategy => momentum::setup(config).await?, }; looper(simulation.agents, simulation.steps).await?; Ok(()) diff --git a/simulation/src/simulations/momentum.rs b/simulation/src/simulations/momentum.rs new file mode 100644 index 000000000..2220872d0 --- /dev/null +++ b/simulation/src/simulations/momentum.rs @@ -0,0 +1,62 @@ +use arbiter_core::environment::builder::BlockSettings; + +use super::{errors::SimulationError, *}; +use crate::{ + agents::{ + arbitrageur::Arbitrageur, block_admin::BlockAdmin, liquidity_provider::LiquidityProvider, + price_changer::PriceChanger, token_admin::TokenAdmin, momentum_strategist::MomentumStrategist, Agent, + Agents, + }, + bindings::i_strategy::IStrategy, + settings::SimulationConfig, +}; + +pub async fn setup(config: SimulationConfig) -> Result { + let environment = EnvironmentBuilder::new() + .block_settings(BlockSettings::UserControlled) + .build(); + let mut block_admin = BlockAdmin::new(&environment, &config).await?; + + let token_admin = TokenAdmin::new(&environment).await?; + let mut price_changer = PriceChanger::new(&environment, &token_admin, &config).await?; + let mut weight_changer = MomentumStrategist::new( + &environment, + &config, + price_changer.liquid_exchange.address(), + token_admin.arbx.address(), + token_admin.arby.address(), + ) + .await?; + + let mut lp = LiquidityProvider::>::new( + &environment, + &token_admin, + weight_changer.g3m.address(), + &config, + ) + .await?; + let mut arbitrageur = Arbitrageur::>::new( + &environment, + &token_admin, + weight_changer.lex.address(), + weight_changer.g3m.address(), + ) + .await?; + + EventLogger::builder() + .path(config.output_directory) + .add(price_changer.liquid_exchange.events(), "lex") + .add(weight_changer.g3m.events(), "g3m") + .run()?; + + Ok(Simulation { + agents: Agents::new() + .add(price_changer) + .add(arbitrageur) + .add(block_admin) + .add(weight_changer) + .add(lp), + steps: config.trajectory.num_steps, + environment, + }) +}