diff --git a/zrml/rikiddo/src/constants.rs b/zrml/rikiddo/src/constants.rs index e4031eb2ec..2d9ffec014 100644 --- a/zrml/rikiddo/src/constants.rs +++ b/zrml/rikiddo/src/constants.rs @@ -18,14 +18,6 @@ pub const EMA_LONG: Timespan = Timespan::Hours(6); pub const SMOOTHING: FixedU32 = >::from_bits(0x0200_0000); // --- Default configuration for FeeSigmoidConfig struct --- -/// Initial fee f -/// 0.005 -pub const INITIAL_FEE: FixedU32 = >::from_bits(0x0147_AE14); - -/// Minimal revenue w (proportion of initial fee f) -/// f * β = 0.005 * 0.7 = 0.0035 -pub const MINIMAL_REVENUE: FixedU32 = >::from_bits(0x00E5_6042); - /// m value /// 0.01 pub const M: FixedI32 = >::from_bits(0x0002_8F5C); @@ -37,3 +29,12 @@ pub const P: FixedI32 = >::from_bits(0x0200_0000); /// n value /// 0.0 pub const N: FixedI32 = >::from_bits(0x0000_0000); + +// --- Default configuration for RikiddoConfig struct --- +/// Initial fee f +/// 0.005 +pub const INITIAL_FEE: FixedU32 = >::from_bits(0x0147_AE14); + +/// Minimal revenue w (proportion of initial fee f) +/// f * β = 0.005 * 0.7 = 0.0035 +pub const MINIMAL_REVENUE: FixedU32 = >::from_bits(0x00E5_6042); diff --git a/zrml/rikiddo/src/tests.rs b/zrml/rikiddo/src/tests.rs index bc9e18ba83..81ee680f3c 100644 --- a/zrml/rikiddo/src/tests.rs +++ b/zrml/rikiddo/src/tests.rs @@ -1,12 +1,66 @@ #![cfg(test)] -use crate::mock::*; +use frame_support::assert_err; +use substrate_fixed::{ + types::extra::{U1, U2, U3, U7, U8}, + FixedI8, FixedU8, +}; + +use crate::{ + mock::*, + types::{convert_to_signed, convert_to_unsigned}, +}; mod ema_market_volume; +mod rikiddo_sigmoid_mv; mod sigmoid_fee; -fn max_allowed_error(fixed_point_bits: u8) -> f64 { - 1.0 / (1u128 << (fixed_point_bits - 1)) as f64 +fn max_allowed_error(fractional_bits: u8) -> f64 { + 1.0 / (1u128 << (fractional_bits - 1)) as f64 +} + +#[test] +fn convert_signed_to_unsigned_fails() { + let num = >::from_num(-0.5f32); + assert_err!( + convert_to_unsigned::, FixedU8>(num), + "Cannot convert negative signed number into unsigned number" + ); +} + +#[test] +fn convert_number_does_not_fit_in_destination_type() { + let num = >::from_num(1); + assert_err!( + convert_to_signed::, FixedI8>(num), + "Fixed point conversion failed: FROM type does not fit in TO type" + ); +} + +#[test] +fn convert_unsigned_to_signed_returns_correct_result() -> Result<(), &'static str> { + // lossless - exact fit + let num1 = >::from_num(4.75); + let num1_converted: FixedI8 = convert_to_signed(num1)?; + assert_eq!(num1_converted, num1); + // lossy - loses fractional bits + let num2 = >::from_num(4.75); + let num2_converted: FixedI8 = convert_to_signed(num2)?; + assert_eq!(num2_converted.to_num::(), 4.5f32); + Ok(()) +} + +#[test] +fn convert_signed_to_unsigned_returns_correct_result() -> Result<(), &'static str> { + // lossless - exact fit + let num1 = >::from_num(4.75); + let num1_converted: FixedU8 = convert_to_unsigned(num1)?; + assert_eq!(num1_converted, num1); + // lossy - loses fractional bits + let num2 = >::from_num(4.75); + let num2_converted: FixedU8 = convert_to_unsigned(num2)?; + assert_eq!(num2_converted.to_num::(), 4.5f32); + Ok(()) } #[test] diff --git a/zrml/rikiddo/src/tests/ema_market_volume.rs b/zrml/rikiddo/src/tests/ema_market_volume.rs index 83b896bdb6..68b656c3fd 100644 --- a/zrml/rikiddo/src/tests/ema_market_volume.rs +++ b/zrml/rikiddo/src/tests/ema_market_volume.rs @@ -1,14 +1,21 @@ use super::max_allowed_error; use crate::{ traits::MarketAverage, - types::{EmaMarketVolume, EmaVolumeConfig, MarketVolumeState, Timespan, TimestampedVolume}, + types::{EmaConfig, EmaMarketVolume, MarketVolumeState, Timespan, TimestampedVolume}, }; use frame_support::assert_err; -use substrate_fixed::{types::extra::U64, FixedU128}; +use substrate_fixed::{ + types::extra::{U64, U96}, + FixedU128, +}; -fn ema_create_test_struct(period: u32, smoothing: f64) -> EmaMarketVolume> { - let emv_cfg = EmaVolumeConfig::> { +pub(super) fn ema_create_test_struct( + period: u32, + smoothing: f64, +) -> EmaMarketVolume> { + let emv_cfg = EmaConfig::> { ema_period: Timespan::Seconds(period), + ema_period_estimate_after: None, smoothing: >::from_num(smoothing), }; @@ -27,9 +34,9 @@ fn ema_calculate(old_ema: f64, multiplier: f64, volume: f64) -> f64 { fn ema_state_transitions_work() { let mut emv = ema_create_test_struct(2, 2.0); assert_eq!(emv.state(), &MarketVolumeState::Uninitialized); - let _ = emv.update(TimestampedVolume { timestamp: 0, volume: 1u32.into() }).unwrap(); + let _ = emv.update(&TimestampedVolume { timestamp: 0, volume: 1u32.into() }).unwrap(); assert_eq!(emv.state(), &MarketVolumeState::DataCollectionStarted); - let _ = emv.update(TimestampedVolume { timestamp: 3, volume: 1u32.into() }).unwrap(); + let _ = emv.update(&TimestampedVolume { timestamp: 3, volume: 1u32.into() }).unwrap(); assert_eq!(emv.state(), &MarketVolumeState::DataCollected); } @@ -37,23 +44,23 @@ fn ema_state_transitions_work() { fn ema_returns_none_before_final_state() { let mut emv = ema_create_test_struct(2, 2.0); assert_eq!(emv.get(), None); - let _ = emv.update(TimestampedVolume { timestamp: 0, volume: 1u32.into() }).unwrap(); + let _ = emv.update(&TimestampedVolume { timestamp: 0, volume: 1u32.into() }).unwrap(); assert_eq!(emv.get(), None); - let _ = emv.update(TimestampedVolume { timestamp: 3, volume: 1u32.into() }).unwrap(); + let _ = emv.update(&TimestampedVolume { timestamp: 3, volume: 1u32.into() }).unwrap(); assert_ne!(emv.get(), None); } #[test] fn ema_returns_correct_ema() { let mut emv = ema_create_test_struct(2, 2.0); - let _ = emv.update(TimestampedVolume { timestamp: 0, volume: 2u32.into() }).unwrap(); - let _ = emv.update(TimestampedVolume { timestamp: 1, volume: 6u32.into() }).unwrap(); - let _ = emv.update(TimestampedVolume { timestamp: 2, volume: 4u32.into() }).unwrap(); + let _ = emv.update(&TimestampedVolume { timestamp: 0, volume: 2u32.into() }).unwrap(); + let _ = emv.update(&TimestampedVolume { timestamp: 1, volume: 6u32.into() }).unwrap(); + let _ = emv.update(&TimestampedVolume { timestamp: 2, volume: 4u32.into() }).unwrap(); // Currently it's a sma let ema = emv.ema.to_num::(); assert_eq!(ema, (2.0 + 6.0 + 4.0) / 3.0); - let _ = emv.update(TimestampedVolume { timestamp: 3, volume: 20u32.into() }).unwrap(); + let _ = emv.update(&TimestampedVolume { timestamp: 3, volume: 20u32.into() }).unwrap(); // Now it's an ema let ema_fixed_f64: f64 = emv.ema.to_num(); let multiplier = ema_get_multiplier(3, emv.config.smoothing.to_num()); @@ -81,11 +88,46 @@ fn ema_returns_correct_ema() { ); } +#[test] +fn ema_returns_correct_ema_after_estimated_period() { + let mut emv = EmaMarketVolume::new(EmaConfig::> { + ema_period: Timespan::Seconds(8), + ema_period_estimate_after: Some(Timespan::Seconds(2)), + smoothing: >::from_num(2.0), + }); + let _ = emv.update(&TimestampedVolume { timestamp: 0, volume: 2u32.into() }).unwrap(); + let _ = emv.update(&TimestampedVolume { timestamp: 1, volume: 6u32.into() }).unwrap(); + let _ = emv.update(&TimestampedVolume { timestamp: 2, volume: 4u32.into() }).unwrap(); + // Currently it's a sma + let ema = emv.ema.to_num::(); + assert_eq!(ema, (2.0 + 6.0 + 4.0) / 3.0); + + let _ = emv.update(&TimestampedVolume { timestamp: 3, volume: 20u32.into() }).unwrap(); + // Now it's an ema (using estimated transaction count per period) + let ema_fixed_f64: f64 = emv.ema.to_num(); + let extrapolation_factor = emv.config.ema_period.to_seconds() as f64 + / emv.config.ema_period_estimate_after.unwrap().to_seconds() as f64; + let multiplier = ema_get_multiplier( + (3f64 * extrapolation_factor).ceil() as u64, + emv.config.smoothing.to_num(), + ); + let ema_f64 = ema_calculate(ema, multiplier, 20f64); + let difference_abs = (ema_fixed_f64 - ema_f64).abs(); + assert!( + difference_abs <= max_allowed_error(64), + "\nFixed result: {}\nFloat result: {}\nDifference: {}\nMax_Allowed_Difference: {}", + ema_fixed_f64, + ema_f64, + difference_abs, + max_allowed_error(64) + ); +} + #[test] fn ema_clear_ereases_data() { let mut emv = ema_create_test_struct(2, 2.0); - let _ = emv.update(TimestampedVolume { timestamp: 0, volume: 2u32.into() }).unwrap(); - let _ = emv.update(TimestampedVolume { timestamp: 3, volume: 6u32.into() }).unwrap(); + let _ = emv.update(&TimestampedVolume { timestamp: 0, volume: 2u32.into() }).unwrap(); + let _ = emv.update(&TimestampedVolume { timestamp: 3, volume: 6u32.into() }).unwrap(); emv.clear(); assert_eq!(emv.ema, >::from_num(0)); assert_eq!(emv.multiplier(), &>::from_num(0)); @@ -98,37 +140,47 @@ fn ema_clear_ereases_data() { #[test] fn ema_added_volume_is_older_than_previous() { let mut emv = ema_create_test_struct(2, 2.0); - let _ = emv.update(TimestampedVolume { timestamp: 2, volume: 2u32.into() }).unwrap(); + let _ = emv.update(&TimestampedVolume { timestamp: 2, volume: 2u32.into() }).unwrap(); assert_err!( - emv.update(TimestampedVolume { timestamp: 1, volume: 2u32.into() }), + emv.update(&TimestampedVolume { timestamp: 1, volume: 2u32.into() }), "[EmaMarketVolume] Incoming volume timestamp is older than previous timestamp" ); } #[test] fn ema_overflow_sma_times_vpp() { - let emv_cfg = EmaVolumeConfig::> { - ema_period: Timespan::Seconds(3), - smoothing: >::from_num(2), - }; - // TODO - let mut emv = >>::new(emv_cfg); - let _ = emv.update(TimestampedVolume { timestamp: 0, volume: 2u32.into() }).unwrap(); - let _ = emv.update(TimestampedVolume { timestamp: 1, volume: 6u32.into() }).unwrap(); + let mut emv = ema_create_test_struct(3, 2.0); + let _ = emv.update(&TimestampedVolume { timestamp: 0, volume: 2u32.into() }).unwrap(); + let _ = emv.update(&TimestampedVolume { timestamp: 1, volume: 6u32.into() }).unwrap(); emv.ema = >::from_num(u64::MAX); assert_err!( - emv.update(TimestampedVolume { timestamp: 3, volume: 6u32.into() }), + emv.update(&TimestampedVolume { timestamp: 3, volume: 6u32.into() }), "[EmaMarketVolume] Overflow during calculation: sma * volumes_per_period" ); } #[test] fn ema_overflow_sma_times_vpp_plus_volume() { - let mut emv = ema_create_test_struct(2, -1.0001); - let _ = emv.update(TimestampedVolume { timestamp: 0, volume: 2u32.into() }).unwrap(); + let mut emv = ema_create_test_struct(2, 2.0); + let _ = emv.update(&TimestampedVolume { timestamp: 0, volume: 2u32.into() }).unwrap(); let max_u64_fixed = >::from_num(u64::MAX); assert_err!( - emv.update(TimestampedVolume { timestamp: 2, volume: max_u64_fixed }), + emv.update(&TimestampedVolume { timestamp: 2, volume: max_u64_fixed }), "[EmaMarketVolume] Overflow during calculation: sma * volumes_per_period + volume" ); } + +#[test] +fn ema_overflow_estimated_tx_per_period_does_not_fit() { + let mut emv = EmaMarketVolume::new(EmaConfig::> { + ema_period: Timespan::Hours(2_386_093), + ema_period_estimate_after: Some(Timespan::Seconds(0)), + smoothing: >::from_num(2.0), + }); + let _ = emv.update(&TimestampedVolume { timestamp: 0, volume: 2u32.into() }).unwrap(); + let _ = emv.update(&TimestampedVolume { timestamp: 0, volume: 2u32.into() }).unwrap(); + assert_err!( + emv.update(&TimestampedVolume { timestamp: 1, volume: 6u32.into() }), + "[EmaMarketVolume] Overflow during estimation of transactions per period" + ); +} diff --git a/zrml/rikiddo/src/tests/rikiddo_sigmoid_mv.rs b/zrml/rikiddo/src/tests/rikiddo_sigmoid_mv.rs new file mode 100644 index 0000000000..f0d9f134c4 --- /dev/null +++ b/zrml/rikiddo/src/tests/rikiddo_sigmoid_mv.rs @@ -0,0 +1,378 @@ +use frame_support::assert_err; +use substrate_fixed::{traits::ToFixed, types::extra::U64, FixedI128, FixedU128}; + +use super::{ema_market_volume::ema_create_test_struct, max_allowed_error}; +use crate::{ + constants::INITIAL_FEE, + traits::{Lmsr, MarketAverage, RikiddoMV}, + types::{ + convert_to_signed, EmaMarketVolume, FeeSigmoid, RikiddoConfig, RikiddoSigmoidMV, + TimestampedVolume, + }, +}; + +type Rikiddo = RikiddoSigmoidMV< + FixedU128, + FixedI128, + FeeSigmoid>, + EmaMarketVolume>, +>; + +fn ln_exp_sum(exponents: &Vec) -> f64 { + exponents.iter().fold(0f64, |acc, val| acc + val.exp()).ln() +} + +fn cost(fee: f64, balances: &Vec) -> f64 { + let fee_times_sum = fee * balances.iter().sum::(); + let exponents = balances.iter().map(|e| e / fee_times_sum).collect(); + fee_times_sum * ln_exp_sum(&exponents) +} + +#[test] +fn rikiddo_updates_mv_and_returns_some() { + let emv = ema_create_test_struct(1, 2.0); + let mut rikiddo = + Rikiddo::new(RikiddoConfig::default(), FeeSigmoid::default(), emv.clone(), emv); + rikiddo.update(&TimestampedVolume { timestamp: 0, volume: 1u32.into() }).unwrap(); + let res = rikiddo.update(&TimestampedVolume { timestamp: 2, volume: 2u32.into() }).unwrap(); + assert_eq!(res, Some(1u32.into())); +} + +#[test] +fn rikiddo_updates_mv_and_returns_none() { + let mut rikiddo = Rikiddo::default(); + let vol = TimestampedVolume::default(); + assert_eq!(rikiddo.update(&vol).unwrap(), None); +} + +#[test] +fn rikiddo_clear_clears_market_data() { + let mut rikiddo = Rikiddo::default(); + let rikiddo_clone = rikiddo.clone(); + let _ = rikiddo.update(&>>::default()); + assert_ne!(rikiddo, rikiddo_clone); + rikiddo.clear(); + assert_eq!(rikiddo, rikiddo_clone); +} + +#[test] +fn rikiddo_get_fee_catches_zero_divison() { + let emv = ema_create_test_struct(1, 0.0); + let mut rikiddo = + Rikiddo::new(RikiddoConfig::default(), FeeSigmoid::default(), emv.clone(), emv); + let _ = rikiddo.update(&TimestampedVolume { timestamp: 0, volume: 0u32.into() }); + let _ = rikiddo.update(&TimestampedVolume { timestamp: 2, volume: 0u32.into() }); + assert_err!( + rikiddo.get_fee(), + "[RikiddoSigmoidMV] Zero division error during calculation: ma_short / ma_long" + ); +} + +#[test] +fn rikiddo_get_fee_overflows_during_ratio_calculation() { + let emv = ema_create_test_struct(1, 2.0); + let mut rikiddo = + Rikiddo::new(RikiddoConfig::default(), FeeSigmoid::default(), emv.clone(), emv); + let _ = rikiddo.update(&TimestampedVolume { timestamp: 0, volume: 0u32.into() }); + let _ = rikiddo.update(&TimestampedVolume { timestamp: 2, volume: 0u32.into() }); + rikiddo.ma_short.ema = >::from_num(u64::MAX); + rikiddo.ma_long.ema = >::from_num(0.1f64); + assert_err!( + rikiddo.get_fee(), + "[RikiddoSigmoidMV] Overflow during calculation: ma_short / ma_long" + ); +} + +#[test] +fn rikiddo_get_fee_ratio_does_not_fit_in_type() { + let emv = ema_create_test_struct(1, 2.0); + let mut rikiddo = + Rikiddo::new(RikiddoConfig::default(), FeeSigmoid::default(), emv.clone(), emv); + let _ = rikiddo.update(&TimestampedVolume { timestamp: 0, volume: 0u32.into() }); + let _ = rikiddo.update(&TimestampedVolume { timestamp: 2, volume: 0u32.into() }); + rikiddo.ma_short.ema = >::from_num(u64::MAX); + rikiddo.ma_long.ema = >::from_num(1u64); + assert_err!( + rikiddo.get_fee(), + "Fixed point conversion failed: FROM type does not fit in TO type" + ); +} + +#[test] +fn rikiddo_get_fee_returns_the_correct_result() { + let emv_short = ema_create_test_struct(1, 2.0); + let emv_long = ema_create_test_struct(2, 2.0); + let mut rikiddo = + Rikiddo::new(RikiddoConfig::default(), FeeSigmoid::default(), emv_short, emv_long); + assert_eq!(rikiddo.ma_short.get(), None); + assert_eq!(rikiddo.ma_long.get(), None); + assert_eq!(rikiddo.get_fee().unwrap(), rikiddo.config.initial_fee); + let _ = rikiddo.update(&TimestampedVolume { timestamp: 0, volume: 100u32.into() }); + let _ = rikiddo.update(&TimestampedVolume { timestamp: 2, volume: 100u32.into() }); + assert_ne!(rikiddo.ma_short.get(), None); + assert_eq!(rikiddo.ma_long.get(), None); + assert_eq!(rikiddo.get_fee().unwrap(), rikiddo.config.initial_fee); + let _ = rikiddo.update(&TimestampedVolume { timestamp: 3, volume: 100u32.into() }); + assert_ne!(rikiddo.ma_short.get(), None); + assert_ne!(rikiddo.ma_long.get(), None); + // We don't want to test the exact result (that is the responsibility of the fee module), + // but rather if rikiddo toggles properly between initial fee and the calculated fee + assert_ne!(rikiddo.get_fee().unwrap(), rikiddo.config.initial_fee); +} + +#[test] +fn rikiddo_default_does_not_panic() -> Result<(), &'static str> { + let default_rikiddo = Rikiddo::default(); + let rikiddo = Rikiddo::new( + RikiddoConfig::new(convert_to_signed(INITIAL_FEE)?), + FeeSigmoid::default(), + EmaMarketVolume::default(), + EmaMarketVolume::default(), + ); + assert_eq!(default_rikiddo, rikiddo); + Ok(()) +} + +#[test] +fn rikiddo_default_ln_sum_exp_strategy_exp_i_overflow() { + let rikiddo = Rikiddo::default(); + let param = vec![>::from_num(100u64)]; + assert_err!( + rikiddo.default_cost_strategy(¶m), + "[RikiddoSigmoidMV] Error during calculation: exp(i) in ln sum_i(exp^i)" + ); +} + +#[test] +fn rikiddo_default_ln_sum_exp_strategy_sum_exp_i_overflow() { + let rikiddo = Rikiddo::default(); + let exponent = >::from_num(42.7f64); + let param = vec![exponent, exponent, exponent]; + assert_err!( + rikiddo.default_cost_strategy(¶m), + "[RikiddoSigmoidMV] Overflow during calculation: sum_i(e^i)" + ); +} + +#[test] +fn rikiddo_default_ln_sum_exp_strategy_ln_zero() { + let rikiddo = Rikiddo::default(); + let param = vec![]; + assert_err!( + rikiddo.default_cost_strategy(¶m), + "[RikiddoSigmoidMV] ln(exp_sum), exp_sum <= 0" + ); +} + +#[test] +fn rikiddo_optimized_ln_sum_exp_strategy_exponent_subtract_overflow() { + let rikiddo = Rikiddo::default(); + let param = vec![>::from_num(1i64 << 63)]; + assert_err!( + rikiddo.optimized_cost_strategy(¶m, &>::from_num(1i64 << 62)), + "[RikiddoSigmoidFee] Overflow during calculation: current_exponent - biggest_exponent" + ); +} + +#[test] +fn rikiddo_optimized_ln_sum_exp_strategy_sum_exp_i_overflow() { + let rikiddo = Rikiddo::default(); + let exponent = >::from_num(42.7f64); + let param = vec![exponent, exponent, exponent]; + assert_err!( + rikiddo.optimized_cost_strategy(¶m, &>::from_num(0)), + "[RikiddoSigmoidFee] Overflow during calculation: sum_i(e^(i - biggest_exponent))" + ); +} + +#[test] +fn rikiddo_optimized_ln_sum_exp_strategy_result_overflow() { + let rikiddo = Rikiddo::default(); + let biggest_exponent = >::from_num(i64::MAX); + let exponent = biggest_exponent - >::from_num(0.0000001f64); + let param = vec![exponent, exponent, exponent]; + assert_err!( + rikiddo.optimized_cost_strategy(¶m, &biggest_exponent), + "[RikiddoSigmoidMV] Overflow during calculation: biggest_exponent + ln(exp_sum) \ + (optimized)" + ); +} + +#[test] +fn rikiddo_ln_sum_exp_strategies_return_correct_results() -> Result<(), &'static str> { + let rikiddo = Rikiddo::default(); + let exponent0 = 3.5f64; + let exponent1 = 4.5f64; + let exponent2 = 5.5f64; + let param_f64 = vec![exponent0, exponent1, exponent2]; + let param_fixed = vec![ + >::from_num(exponent0), + >::from_num(exponent1), + >::from_num(exponent2), + ]; + // Evaluate the result of the default cost strategy + let mut result_fixed = rikiddo.default_cost_strategy(¶m_fixed)?; + let result_f64: f64 = ln_exp_sum(¶m_f64); + let mut result_fixed_f64: f64 = result_fixed.to_num(); + let mut difference_abs = (result_f64 - result_fixed_f64).abs(); + // The fixed calculation seems to be quite errorneous: Difference = 0.00000007886511177446209 + assert!( + difference_abs <= 0.000001f64, + "\nFixed result: {}\nFloat result: {}\nDifference: {}\nMax_Allowed_Difference: {}", + result_fixed_f64, + result_f64, + difference_abs, + max_allowed_error(64) + ); + + // Evaluate the result of the optimize cost strategy + result_fixed = rikiddo.optimized_cost_strategy(¶m_fixed, ¶m_fixed[2])?; + result_fixed_f64 = result_fixed.to_num(); + difference_abs = (result_f64 - result_fixed_f64).abs(); + assert!( + difference_abs <= 0.00000001f64, + "\nFixed result: {}\nFloat result: {}\nDifference: {}\nMax_Allowed_Difference: {}", + result_fixed_f64, + result_f64, + difference_abs, + max_allowed_error(64) + ); + + Ok(()) +} + +#[test] +fn rikiddo_cost_function_rejects_empty_list() { + let rikiddo = Rikiddo::default(); + assert_err!(rikiddo.cost(&vec![]), "[RikiddoSigmoidMV] No asset balances provided"); +} + +#[test] +fn rikiddo_cost_function_overflow_during_summation_of_balances() { + let rikiddo = Rikiddo::default(); + let exponent = >::from_num(u64::MAX); + let param = vec![exponent, exponent]; + assert_err!( + rikiddo.cost(¶m), + "[RikiddoSigmoidMV] Overflow during summation of asset balances" + ); +} + +#[test] +fn rikiddo_cost_function_overflow_during_fee_times_balance_sum() { + let mut rikiddo = Rikiddo::default(); + rikiddo.config.initial_fee = >::from_num(2); + let param = >::from_num(u64::MAX); + assert_err!( + rikiddo.cost(&vec![param]), + "[RikiddoSigmoidMV] Overflow during calculation: fee * total_asset_balance" + ); +} + +#[test] +fn rikiddo_cost_function_overflow_during_calculation_of_exponent() { + let mut rikiddo = Rikiddo::default(); + rikiddo.config.initial_fee = + >::from_bits(0x0000_0000_0000_0000_0000_0000_0000_0001); + let param = >::from_num(u64::MAX); + assert_err!( + rikiddo.cost(&vec![param]), + "[RikiddoSigmoidMV] Overflow during calculation: expontent_i = asset_balance_i / \ + denominator" + ); +} + +#[test] +fn rikiddo_cost_function_overflow_during_log2e_times_biggest_exponent() { + let mut rikiddo = Rikiddo::default(); + rikiddo.config.initial_fee = + >::from_bits(0x0000_0000_0000_0000_0000_0000_0000_0003); + rikiddo.config.log2_e = >::from_num(i64::MAX); + let param = >::from_num(i64::MAX as u64); + assert_err!( + rikiddo.cost(&vec![param]), + "[RikiddoSigmoidMV] Overflow during calculation: log2_e * biggest_exponent" + ); +} + +#[test] +fn rikiddo_cost_function_overflow_during_calculation_of_required_bits_minus_one() { + let mut rikiddo = Rikiddo::default(); + rikiddo.config.initial_fee = >::from_num(1); + rikiddo.config.log2_e = >::from_num(i64::MAX); + let param = >::from_num(i64::MAX as u64); + let zero = >::from_num(0); + assert_err!( + rikiddo.cost(&vec![param, zero]), + "[RikiddoSigmoidMV] Overflow during calculation: biggest_exp * log2(e) + log2(num_assets)" + ); +} + +#[test] +fn rikiddo_cost_function_overflow_during_ceil_required_bits_minus_one() { + let mut rikiddo = Rikiddo::default(); + rikiddo.config.initial_fee = >::from_num(1); + rikiddo.config.log2_e = >::from_num(i64::MAX) + >::from_num(0.1); + let param = >::from_num(i64::MAX as u64); + assert_err!( + rikiddo.cost(&vec![param]), + "[RikiddoSigmoidMV] Overflow during calculation: ceil(biggest_exp * log2(e) + \ + log2(num_assets))" + ); +} + +#[test] +fn rikiddo_cost_function_overflow_during_calculation_of_result() { + let mut rikiddo = Rikiddo::default(); + rikiddo.config.initial_fee = >::from_num(1); + rikiddo.config.log2_e = >::from_num(i64::MAX); + let param = >::from_num(i64::MAX as u64); + assert_err!( + rikiddo.cost(&vec![param, param]), + "[RikiddoSigmoidMV] Overflow during calculation: fee * total_asset_balance * \ + ln(sum_i(e^i))" + ); +} + +#[test] +fn rikiddo_cost_function_correct_result() -> Result<(), &'static str> { + let mut rikiddo = Rikiddo::default(); + // Evaluate the cost using the optimized strategy + let balance0 = 3.5f64; + let balance1 = 3.6f64; + let balance2 = 3.7f64; + let param_f64 = vec![balance0, balance1, balance2]; + let param_fixed = vec![ + >::from_num(balance0), + >::from_num(balance1), + >::from_num(balance2), + ]; + let mut result_fixed = rikiddo.cost(¶m_fixed)?; + let mut result_f64: f64 = cost(rikiddo.config.initial_fee.to_num(), ¶m_f64); + let mut result_fixed_f64: f64 = result_fixed.to_num(); + let mut difference_abs = (result_f64 - result_fixed_f64).abs(); + assert!( + difference_abs <= 0.0000001f64, + "\nFixed result: {}\nFloat result: {}\nDifference: {}\nMax_Allowed_Difference: {}", + result_fixed_f64, + result_f64, + difference_abs, + max_allowed_error(64) + ); + + // Evaluate the cost using the default strategy + rikiddo.config.initial_fee = 0.1.to_fixed(); + result_f64 = cost(rikiddo.config.initial_fee.to_num(), ¶m_f64); + result_fixed = rikiddo.cost(¶m_fixed)?; + result_fixed_f64 = result_fixed.to_num(); + difference_abs = (result_f64 - result_fixed_f64).abs(); + assert!( + difference_abs <= 0.0000001f64, + "\nFixed result: {}\nFloat result: {}\nDifference: {}\nMax_Allowed_Difference: {}", + result_fixed_f64, + result_f64, + difference_abs, + max_allowed_error(64) + ); + Ok(()) +} diff --git a/zrml/rikiddo/src/tests/sigmoid_fee.rs b/zrml/rikiddo/src/tests/sigmoid_fee.rs index 11b1b28d10..cbf54c3242 100644 --- a/zrml/rikiddo/src/tests/sigmoid_fee.rs +++ b/zrml/rikiddo/src/tests/sigmoid_fee.rs @@ -14,11 +14,15 @@ fn init_default_sigmoid_fee_struct() -> (FeeSigmoid>, f64, f64, f let m = 0.01f64; let n = 0f64; let p = 2.0f64; + let initial_fee = 0.005; + let min_revenue = 0.0035; let config = FeeSigmoidConfig { m: >::from_num(m), n: >::from_num(n), p: >::from_num(p), + initial_fee: >::from_num(initial_fee), + min_revenue: >::from_num(min_revenue), }; let fee = FeeSigmoid { config }; @@ -76,20 +80,23 @@ fn fee_sigmoid_overflow_numerator_div_denominator() { #[test] fn fee_sigmoid_correct_result() -> Result<(), &'static str> { let r = 1.5f64; - let (fee, m, n, p) = init_default_sigmoid_fee_struct(); - let fee_f64 = sigmoid_fee(m, n, p, r); - let fee_fixed = fee.calculate(>::from_num(r))?; + let (mut fee, m, n, p) = init_default_sigmoid_fee_struct(); + let fee_f64 = fee.config.initial_fee.to_num::() + sigmoid_fee(m, n, p, r); + let r_fixed = >::from_num(r); + let fee_fixed = fee.calculate(r_fixed)?; let fee_fixed_f64: f64 = fee_fixed.to_num(); let difference_abs = (fee_f64 - fee_fixed_f64).abs(); assert!( difference_abs <= max_allowed_error(64), "\nFixed result: {}\nFloat result: {}\nDifference: {}\nMax_Allowed_Difference: {}", - fee_f64, fee_fixed_f64, + fee_f64, difference_abs, max_allowed_error(64) ); + fee.config.min_revenue = >::from_num(1u64 << 62); + assert_eq!(fee.calculate(r_fixed)?, fee.config.min_revenue); Ok(()) } diff --git a/zrml/rikiddo/src/traits.rs b/zrml/rikiddo/src/traits.rs index 13467ef8ae..2b48a5dd6c 100644 --- a/zrml/rikiddo/src/traits.rs +++ b/zrml/rikiddo/src/traits.rs @@ -1,4 +1,4 @@ -use crate::types::{EmaVolumeConfig, FeeSigmoidConfig, TimestampedVolume}; +use crate::types::{EmaConfig, FeeSigmoidConfig, TimestampedVolume}; use frame_support::{ pallet_prelude::{MaybeSerializeDeserialize, Member}, Parameter, @@ -8,42 +8,58 @@ use sp_runtime::traits::AtLeast32BitUnsigned; use sp_std::fmt::Debug; use substrate_fixed::traits::{Fixed, FixedSigned, FixedUnsigned}; -pub trait Sigmoid { +pub trait Sigmoid { + type FS: Fixed; + /// Calculate fee - fn calculate(&self, r: F) -> Result; + fn calculate(&self, r: Self::FS) -> Result; } -pub trait MarketAverage { +pub trait MarketAverage { + type FU: FixedUnsigned; + /// Get average (sma, ema, wma, depending on the concrete implementation) of market volume - fn get(&self) -> Option; + fn get(&self) -> Option; /// Clear market data fn clear(&mut self); /// Update market volume - fn update(&mut self, volume: TimestampedVolume) -> Result, &'static str>; + fn update( + &mut self, + volume: &TimestampedVolume, + ) -> Result, &'static str>; } -pub trait Lmsr { +pub trait Lmsr { + type FU: FixedUnsigned; + /// Return price P_i(q) for all assets in q - fn all_prices(asset_balances: Vec) -> Result, &'static str>; + fn all_prices(&self, asset_balances: &[Self::FU]) -> Result, &'static str>; /// Return cost C(q) for all assets in q - fn cost(asset_balances: Vec) -> Result; + fn cost(&self, asset_balances: &[Self::FU]) -> Result; /// Return price P_i(q) for asset q_i in q - fn price(asset_in_question_balance: F, asset_balances: Vec) -> Result; + fn price( + &self, + asset_in_question_balance: &Self::FU, + asset_balances: &[Self::FU], + ) -> Result; } -pub trait RikiddoMV: Lmsr { +pub trait RikiddoMV: Lmsr { /// Clear market data fn clear(&mut self); /// Update market data - fn update(&mut self, volume: TimestampedVolume) -> Result, &'static str>; + fn update( + &mut self, + volume: &TimestampedVolume, + ) -> Result, &'static str>; } -pub trait RikiddoSigmoidMVPallet { +pub trait RikiddoSigmoidMVPallet { type Balance: Parameter + Member + AtLeast32BitUnsigned @@ -53,6 +69,9 @@ pub trait RikiddoSigmoidMVPallet { + MaybeSerializeDeserialize + Debug; + type FS: FixedSigned; + type FU: FixedUnsigned; + /// Clear market data for specific asset pool fn clear(poolid: u128); @@ -65,9 +84,9 @@ pub trait RikiddoSigmoidMVPallet { /// Create Rikiddo instance for specifc asset pool fn create( poolid: u128, - fee_config: FeeSigmoidConfig, - ema_config_short: EmaVolumeConfig, - ema_config_long: EmaVolumeConfig, + fee_config: FeeSigmoidConfig, + ema_config_short: EmaConfig, + ema_config_long: EmaConfig, balance_one_unit: Self::Balance, ); diff --git a/zrml/rikiddo/src/types.rs b/zrml/rikiddo/src/types.rs index d4691fba7e..9b8d0621e0 100644 --- a/zrml/rikiddo/src/types.rs +++ b/zrml/rikiddo/src/types.rs @@ -1,23 +1,27 @@ use frame_support::dispatch::{fmt::Debug, Decode, Encode}; -use substrate_fixed::traits::Fixed; - -// use crate::traits::{MarketAverage, Sigmoid}; +use substrate_fixed::{ + traits::{Fixed, FixedSigned, FixedUnsigned, LossyFrom, LossyInto, ToFixed}, + types::extra::{U127, U128}, + FixedI128, FixedU128, +}; mod ema_market_volume; +mod rikiddo_sigmoid_mv; mod sigmoid_fee; pub use ema_market_volume::*; +pub use rikiddo_sigmoid_mv::*; pub use sigmoid_fee::*; pub type UnixTimestamp = u64; -#[derive(Clone, Debug, Decode, Encode, Eq, PartialEq)] +#[derive(Clone, Debug, Decode, Default, Encode, Eq, PartialEq)] pub struct TimestampedVolume { pub timestamp: UnixTimestamp, pub volume: F, } -#[derive(Clone, Debug, Decode, Encode, Eq, PartialEq)] +#[derive(Copy, Clone, Debug, Decode, Encode, Eq, PartialEq, PartialOrd)] pub enum Timespan { Seconds(u32), Minutes(u32), @@ -29,21 +33,67 @@ pub enum Timespan { impl Timespan { pub fn to_seconds(&self) -> u32 { match *self { + // Any value that leads to a saturation is greater than + // 4294967295 seconds, which is about 136 years. Timespan::Seconds(d) => d, - Timespan::Minutes(d) => d * 60, - Timespan::Hours(d) => d * 60 * 60, - Timespan::Days(d) => u32::from(d) * 60 * 60 * 24, - Timespan::Weeks(d) => u32::from(d) * 60 * 60 * 24 * 7, + Timespan::Minutes(d) => d.saturating_mul(60), + Timespan::Hours(d) => d.saturating_mul(60).saturating_mul(60), + Timespan::Days(d) => { + u32::from(d).saturating_mul(60).saturating_mul(60).saturating_mul(24) + } + Timespan::Weeks(d) => u32::from(d) + .saturating_mul(60) + .saturating_mul(60) + .saturating_mul(24) + .saturating_mul(7), } } } -/* -#[derive(Clone, Debug, Decode, Encode, Eq, PartialEq)] -pub struct RikiddoSigmoidMV, MA: MarketAverage> { - pub fees: FE, - pub ma_short: MA, - pub ma_long: MA, - pub _marker: PhantomData, +/// Returns integer part of FROM, if the msb is not set and and num does it into FROM +fn convert_common(num: FROM) -> Result { + // Check if number is negatie + if num < FROM::from_num(0u8) { + return Err("Cannot convert negative signed number into unsigned number"); + } + + // PartialOrd is bugged, therefore the workaround + // https://github.com/encointer/substrate-fixed/issues/9 + let num_u128: u128 = num.int().to_num(); + if TO::max_value().int().to_num::() < num_u128 { + return Err("Fixed point conversion failed: FROM type does not fit in TO type"); + } + + Ok(num_u128.to_fixed()) +} + +/// Converts an unsigned fixed point number into a signed fixed point number (fallible) +pub fn convert_to_signed>>( + num: FROM, +) -> Result { + let integer_part: TO = convert_common(num)?; + let fractional_part: FixedI128 = num.frac().to_fixed(); + + if let Some(res) = integer_part.checked_add(fractional_part.lossy_into()) { + Ok(res) + } else { + // This error should be impossible to reach. + Err("Something went wrong during FixedUnsigned to FixedSigned type conversion") + } +} + +/// Converts a signed fixed point number into an unsigned fixed point number (fallible) +pub fn convert_to_unsigned>>( + num: FROM, +) -> Result { + // We can safely cast because until here we know that the msb is not set. + let integer_part: TO = convert_common(num)?; + let fractional_part: FixedU128 = num.frac().to_fixed(); + + if let Some(res) = integer_part.checked_add(fractional_part.lossy_into()) { + Ok(res) + } else { + // This error should be impossible to reach. + Err("Something went wrong during FixedSigned to FixedUnsigned type conversion") + } } -*/ diff --git a/zrml/rikiddo/src/types/ema_market_volume.rs b/zrml/rikiddo/src/types/ema_market_volume.rs index 8bea7ab6e7..3c03ec5ad9 100644 --- a/zrml/rikiddo/src/types/ema_market_volume.rs +++ b/zrml/rikiddo/src/types/ema_market_volume.rs @@ -5,26 +5,35 @@ use crate::{ }; use frame_support::dispatch::{fmt::Debug, Decode, Encode}; use substrate_fixed::{ - traits::{Fixed, FixedUnsigned, LossyFrom, LossyInto}, - types::extra::U24, - FixedU32, + traits::{Fixed, FixedUnsigned, LossyFrom, LossyInto, ToFixed}, + types::extra::{U24, U64}, + FixedU128, FixedU32, }; #[derive(Clone, Debug, Decode, Encode, Eq, PartialEq)] -pub struct EmaVolumeConfig { +pub struct EmaConfig { pub ema_period: Timespan, - pub smoothing: F, + pub ema_period_estimate_after: Option, + pub smoothing: FI, } -impl>> EmaVolumeConfig { - pub fn new(ema_period: Timespan, smoothing: F) -> Self { - Self { ema_period, smoothing } +impl>> EmaConfig { + pub fn new( + ema_period: Timespan, + mut ema_period_estimate_after: Option, + smoothing: FU, + ) -> Self { + if let Some(res) = ema_period_estimate_after { + ema_period_estimate_after = if res >= ema_period { None } else { Some(res) }; + }; + + Self { ema_period, ema_period_estimate_after, smoothing } } } -impl>> Default for EmaVolumeConfig { +impl>> Default for EmaConfig { fn default() -> Self { - Self::new(EMA_SHORT, SMOOTHING.lossy_into()) + Self::new(EMA_SHORT, None, SMOOTHING.lossy_into()) } } @@ -36,30 +45,33 @@ pub enum MarketVolumeState { } #[derive(Clone, Debug, Decode, Encode, Eq, PartialEq)] -pub struct EmaMarketVolume { - pub config: EmaVolumeConfig, - pub ema: F, - multiplier: F, +pub struct EmaMarketVolume { + pub config: EmaConfig, + pub ema: FU, + multiplier: FU, last_time: UnixTimestamp, state: MarketVolumeState, start_time: UnixTimestamp, - volumes_per_period: F, + volumes_per_period: FU, } -impl EmaMarketVolume { - pub fn new(config: EmaVolumeConfig) -> Self { +impl EmaMarketVolume { + pub fn new(config: EmaConfig) -> Self { Self { config, - ema: F::from_num(0), + ema: FU::from_num(0), state: MarketVolumeState::Uninitialized, start_time: 0, last_time: 0, - volumes_per_period: F::from_num(0), - multiplier: F::from_num(0), + volumes_per_period: FU::from_num(0), + multiplier: FU::from_num(0), } } - fn calculate_ema(&mut self, volume: &TimestampedVolume) -> Result, &'static str> { + fn calculate_ema( + &mut self, + volume: &TimestampedVolume, + ) -> Result, &'static str> { // Overflow is impossible here (the library ensures that multiplier ∊ [0,1]) let volume_times_multiplier = if let Some(res) = volume.volume.checked_mul(self.multiplier) { @@ -69,7 +81,7 @@ impl EmaMarketVolume { }; // Overflow is impossible here. - let one_minus_multiplier = if let Some(res) = F::from_num(1).checked_sub(self.multiplier) { + let one_minus_multiplier = if let Some(res) = FU::from_num(1).checked_sub(self.multiplier) { res } else { return Err("[EmaMarketVolume] Overflow during calculation: 1 - multiplier"); @@ -96,7 +108,10 @@ impl EmaMarketVolume { Ok(Some(self.ema)) } - fn calculate_sma(&mut self, volume: &TimestampedVolume) -> Result, &'static str> { + fn calculate_sma( + &mut self, + volume: &TimestampedVolume, + ) -> Result, &'static str> { // This can only overflow if the ema field is set manually let sma_times_vpp = if let Some(res) = self.ema.checked_mul(self.volumes_per_period) { res @@ -114,7 +129,7 @@ impl EmaMarketVolume { // This can't overflow. self.ema = if let Some(res) = sma_times_vpp_plus_volume - .checked_div(self.volumes_per_period.saturating_add(F::from_num(1))) + .checked_div(self.volumes_per_period.saturating_add(FU::from_num(1))) { res } else { @@ -126,7 +141,7 @@ impl EmaMarketVolume { Ok(Some(self.ema)) } - pub fn multiplier(&self) -> &F { + pub fn multiplier(&self) -> &FU { &self.multiplier } @@ -143,20 +158,22 @@ impl EmaMarketVolume { &self.start_time } - pub fn volumes_per_period(&self) -> &F { + pub fn volumes_per_period(&self) -> &FU { &self.volumes_per_period } } -impl + LossyFrom>> Default for EmaMarketVolume { +impl + LossyFrom>> Default for EmaMarketVolume { fn default() -> Self { - EmaMarketVolume::new(EmaVolumeConfig::default()) + EmaMarketVolume::new(EmaConfig::default()) } } -impl> MarketAverage for EmaMarketVolume { +impl> MarketAverage for EmaMarketVolume { + type FU = FU; + /// Calculate average (sma, ema, wma, depending on the concrete implementation) of market volume - fn get(&self) -> Option { + fn get(&self) -> Option { match &self.state { MarketVolumeState::DataCollected => Some(self.ema), _ => None, @@ -165,16 +182,19 @@ impl> MarketAverage for EmaMarketVolume { /// Clear market data fn clear(&mut self) { - self.ema = F::from_num(0); - self.multiplier = F::from_num(0); + self.ema = FU::from_num(0); + self.multiplier = FU::from_num(0); self.last_time = 0; self.state = MarketVolumeState::Uninitialized; self.start_time = 0; - self.volumes_per_period = F::from_num(0); + self.volumes_per_period = FU::from_num(0); } /// Update market volume - fn update(&mut self, volume: TimestampedVolume) -> Result, &'static str> { + fn update( + &mut self, + volume: &TimestampedVolume, + ) -> Result, &'static str> { if let Some(res) = volume.timestamp.checked_sub(self.last_time) { res } else { @@ -183,7 +203,7 @@ impl> MarketAverage for EmaMarketVolume { ); }; - let mut result: Option = None; + let mut result: Option = None; match self.state { MarketVolumeState::Uninitialized => { @@ -198,32 +218,72 @@ impl> MarketAverage for EmaMarketVolume { // against the timestamp of the previous transaction. let timestamp_sub_start_time = volume.timestamp.saturating_sub(self.start_time); + let mut comparison_value = if let Some(res) = self.config.ema_period_estimate_after + { + res.to_seconds() + } else { + self.config.ema_period.to_seconds() + }; + // It should not state transit, if the amount of gathered data is too low. // This would result in a multiplier that is greater than 1, which can lead to // a negative ema. The amount depends on the size of the smoothing factor. - if timestamp_sub_start_time > self.config.ema_period.to_seconds() as u64 + if timestamp_sub_start_time > comparison_value as u64 && (*self.volumes_per_period() + 1.into()) >= self.config.smoothing { + // We extrapolate the txs per period + if self.config.ema_period_estimate_after.is_some() { + // Ensure that we don't divide by 0 + if comparison_value == 0 { + comparison_value = 1 + } + + let premature_time_fixed = >::from(comparison_value); + let mature_time_fixed = + >::from(self.config.ema_period.to_seconds()); + // Overflow impossible + let estimate_ratio = mature_time_fixed.saturating_div(premature_time_fixed); + + if estimate_ratio.int() > FU::max_value().to_num::() { + // Cannot occur as long as the From trait is required for FU and + // Timespan::to_seconds() returns u32. + return Err("[EmaMarketVolume] Estimate ratio does not fit in FU"); + } + + // Can't panic due to the previous check + let estimate_ratio_fu: FU = estimate_ratio.to_fixed(); + + self.volumes_per_period = if let Some(res) = + self.volumes_per_period.checked_mul(estimate_ratio_fu) + { + res.saturating_ceil() + } else { + return Err("[EmaMarketVolume] Overflow during estimation of \ + transactions per period"); + } + } + // Overflow is impossible here. self.multiplier = if let Some(res) = self .config .smoothing - .checked_div(self.volumes_per_period.saturating_add(F::from(1))) + .checked_div(self.volumes_per_period.saturating_add(FU::from(1))) { res } else { return Err("[EmaMarketVolume] Overflow during calculation: multiplier = \ smoothing / (1 + volumes_per_period)"); }; + self.state = MarketVolumeState::DataCollected; result = self.calculate_ema(&volume)?; } else { // During this phase the ema is still a sma. - result = self.calculate_sma(&volume)?; + let _ = self.calculate_sma(&volume)?; // In the context of blockchains, overflowing here is irrelevant (technically - // not realizable). In other contexts, ensure that F can represent a number + // not realizable). In other contexts, ensure that FU can represent a number // that is equal to the number of incoming volumes during one period. - self.volumes_per_period = self.volumes_per_period.saturating_add(F::from(1)); + self.volumes_per_period = self.volumes_per_period.saturating_add(FU::from(1)); } } MarketVolumeState::DataCollected => { diff --git a/zrml/rikiddo/src/types/rikiddo_sigmoid_mv.rs b/zrml/rikiddo/src/types/rikiddo_sigmoid_mv.rs new file mode 100644 index 0000000000..105b068957 --- /dev/null +++ b/zrml/rikiddo/src/types/rikiddo_sigmoid_mv.rs @@ -0,0 +1,368 @@ +use crate::{ + constants::INITIAL_FEE, + traits::{Lmsr, MarketAverage, RikiddoMV, Sigmoid}, +}; +use core::ops::{AddAssign, BitOrAssign, ShlAssign}; +use frame_support::dispatch::{fmt::Debug, Decode, Encode}; +use substrate_fixed::{ + consts::LOG2_E, + traits::{Fixed, FixedSigned, FixedUnsigned, LossyFrom, LossyInto, ToFixed}, + transcendental::{exp, ln, log2}, + types::{ + extra::{U127, U128, U31, U32}, + I9F23, U1F127, + }, + FixedI128, FixedI32, FixedU128, FixedU32, +}; + +use super::{convert_to_signed, convert_to_unsigned, TimestampedVolume}; + +#[derive(Clone, Debug, Decode, Encode, Eq, PartialEq)] +pub struct RikiddoConfig { + pub initial_fee: FI, + pub(crate) log2_e: FI, +} + +impl> + LossyFrom> RikiddoConfig { + pub fn new(initial_fee: FS) -> Self { + Self { initial_fee, log2_e: FS::lossy_from(LOG2_E) } + } +} + +impl> + LossyFrom> Default for RikiddoConfig { + fn default() -> Self { + // Potentially dangerous unwrap(), should be impossible to fail (tested). + let converted = convert_to_signed::, FixedI32>(INITIAL_FEE).unwrap(); + Self::new(converted.lossy_into()) + } +} +#[derive(Clone, Debug, Decode, Default, Encode, Eq, PartialEq)] +pub struct RikiddoSigmoidMV +where + FU: FixedUnsigned + LossyFrom>, + FS: FixedSigned + LossyFrom> + LossyFrom, + FE: Sigmoid, + MA: MarketAverage, +{ + pub config: RikiddoConfig, + pub fees: FE, + pub ma_short: MA, + pub ma_long: MA, +} + +impl RikiddoSigmoidMV +where + FU: FixedUnsigned + LossyFrom> + LossyFrom>, + FS: FixedSigned + + From + + LossyFrom> + + LossyFrom + + LossyFrom> + + PartialOrd, + FS::Bits: Copy + ToFixed + AddAssign + BitOrAssign + ShlAssign, + FE: Sigmoid, + MA: MarketAverage, +{ + pub fn new(config: RikiddoConfig, fees: FE, ma_short: MA, ma_long: MA) -> Self { + Self { config, fees, ma_short, ma_long } + } + + pub fn get_fee(&self) -> Result { + let mas = if let Some(res) = self.ma_short.get() { + res + } else { + return convert_to_unsigned(self.config.initial_fee); + }; + + let mal = if let Some(res) = self.ma_long.get() { + res + } else { + return convert_to_unsigned(self.config.initial_fee); + }; + + if mal == FU::from_num(0u8) { + return Err( + "[RikiddoSigmoidMV] Zero division error during calculation: ma_short / ma_long" + ); + } + + let ratio = if let Some(res) = mas.checked_div(mal) { + res + } else { + return Err("[RikiddoSigmoidMV] Overflow during calculation: ma_short / ma_long"); + }; + + let ratio_signed = convert_to_signed(ratio)?; + convert_to_unsigned::(self.fees.calculate(ratio_signed)?) + } + + pub(crate) fn default_cost_strategy(&self, exponents: &[FS]) -> Result { + let mut acc: FS = FS::from_num(0u8); + + for elem in exponents { + let exp_value: FS = if let Ok(res) = exp::(*elem) { + res + } else { + return Err( + "[RikiddoSigmoidMV] Error during calculation: exp(i) in ln sum_i(exp^i)" + ); + }; + + if let Some(res) = acc.checked_add(exp_value) { + acc = res; + } else { + // Impossible (this function should only be called when the sum does fit into FS) + return Err("[RikiddoSigmoidMV] Overflow during calculation: sum_i(e^i)"); + }; + } + + if let Ok(res) = ln::(acc) { + Ok(res) + } else { + // Impossible to reach, unless the "exponents" vector is empty + Err("[RikiddoSigmoidMV] ln(exp_sum), exp_sum <= 0") + } + } + + pub(crate) fn optimized_cost_strategy( + &self, + exponents: &[FS], + biggest_exponent: &FS, + ) -> Result { + let mut biggest_exponent_used = false; + + if FS::max_value().int().to_num::() < 1u128 { + // Impossible due to trait bounds (at least 1 sign bit and 8 integer bits) + return Err("[RikiddoSigmoidMV] Error, cannot initialize FS with one"); + } + + let mut exp_sum = FS::from_num(1u8); + let result = *biggest_exponent; + + for elem in exponents { + if !biggest_exponent_used && elem == biggest_exponent { + biggest_exponent_used = true; + continue; + } + + let exponent = if let Some(res) = elem.checked_sub(*biggest_exponent) { + res + } else { + // Should be impossible + return Err("[RikiddoSigmoidFee] Overflow during calculation: current_exponent - \ + biggest_exponent"); + }; + + let e_power_exponent = if let Ok(res) = exp::(exponent) { + res + } else { + // In this case the result is zero (or is too small to fit) and can be ignored + continue; + }; + + if let Some(res) = exp_sum.checked_add(e_power_exponent) { + exp_sum = res; + } else { + // Highly unlikely + return Err("[RikiddoSigmoidFee] Overflow during calculation: sum_i(e^(i - \ + biggest_exponent))"); + }; + } + + let ln_exp_sum = if let Ok(res) = ln::(exp_sum) { + res + } else { + // Impossible + return Err("[RikiddoSigmoidMV] ln(exp_sum) (optimized), exp_sum <= 0"); + }; + + if let Some(res) = result.checked_add(ln_exp_sum) { + Ok(res) + } else { + // Highly unlikely + Err("[RikiddoSigmoidMV] Overflow during calculation: biggest_exponent + ln(exp_sum) \ + (optimized)") + } + } +} + +impl Lmsr for RikiddoSigmoidMV +where + FU: FixedUnsigned + LossyFrom> + LossyFrom>, + FS: FixedSigned + + From + + LossyFrom> + + LossyFrom + + LossyFrom> + + PartialOrd, + FS::Bits: Copy + ToFixed + AddAssign + BitOrAssign + ShlAssign, + FE: Sigmoid, + MA: MarketAverage, +{ + type FU = FU; + + /// Return price P_i(q) for all assets in q + fn all_prices(&self, _asset_balances: &[Self::FU]) -> Result, &'static str> { + Err("Unimplemented") + } + + /// Return cost C(q) for all assets in q + fn cost(&self, asset_balances: &[Self::FU]) -> Result { + if asset_balances.is_empty() { + return Err("[RikiddoSigmoidMV] No asset balances provided"); + }; + + let fee = self.get_fee()?; + let mut total_balance = FU::from_num(0u8); + + for elem in asset_balances { + if let Some(res) = total_balance.checked_add(*elem) { + total_balance = res; + } else { + return Err("[RikiddoSigmoidMV] Overflow during summation of asset balances"); + } + } + + let denominator = if let Some(res) = fee.checked_mul(total_balance) { + res + } else { + // Highly unlikely and only possible if fee > 100% + return Err("[RikiddoSigmoidMV] Overflow during calculation: fee * total_asset_balance"); + }; + + let mut exponents: Vec = Vec::with_capacity(asset_balances.len()); + let mut biggest_exponent: FS = FS::from_num(0u8); + + for elem in asset_balances { + let exponent = if let Some(res) = elem.checked_div(denominator) { + convert_to_signed::(res)? + } else { + // Highly unlikely + return Err("[RikiddoSigmoidMV] Overflow during calculation: expontent_i = \ + asset_balance_i / denominator"); + }; + + if exponent > biggest_exponent { + biggest_exponent = exponent; + } + + // Panic impossible + exponents.push(exponent); + } + + // Determine which strategy to use. + let biggest_exp_times_log2e = if let Some(res) = + self.config.log2_e.checked_mul(biggest_exponent) + { + res + } else { + // Highly unlikely + return Err("[RikiddoSigmoidMV] Overflow during calculation: log2_e * biggest_exponent"); + }; + + if FS::max_value().int().to_num::() < asset_balances.len() as u128 { + return Err("[RikidoSigmoidMV] Number of assets does not fit in FS"); + } + + // Panic impossible + let log2_number_of_assets: FS = + if let Ok(res) = log2::(FS::from_num(asset_balances.len())) { + res + } else { + // Impossible, since the cost functions checks if elements are present in asset_balances + return Err("[RikiddoSigmoidMV] log2(number_of_assets), number_of_assets <= 0"); + }; + + let log2e_times_biggest_exp_plus_log2_num_assets = + if let Some(res) = log2_number_of_assets.checked_add(biggest_exp_times_log2e) { + res + } else { + // Highly unlikely + return Err("[RikiddoSigmoidMV] Overflow during calculation: biggest_exp * \ + log2(e) + log2(num_assets)"); + }; + + let required_bits_minus_one: u128 = + if let Some(res) = log2e_times_biggest_exp_plus_log2_num_assets.checked_ceil() { + res.to_num() + } else { + // Highly unlikely + return Err("[RikiddoSigmoidMV] Overflow during calculation: ceil(biggest_exp * \ + log2(e) + log2(num_assets))"); + }; + + let required_bits = if let Some(res) = required_bits_minus_one.checked_add(1) { + res + } else { + // Overflow impossible + return Err( + "[RikiddoSigmoidMV] Overflow during calculation: required_bits_minus_one + 1" + ); + }; + + let ln_sum_e: FS; + + // Select strategy to calculate ln(sum_i(e^i)) + if required_bits > FS::int_nbits() as u128 { + ln_sum_e = self.optimized_cost_strategy(&exponents, &biggest_exponent)?; + } else { + ln_sum_e = self.default_cost_strategy(&exponents)?; + } + + if let Some(res) = denominator.checked_mul(convert_to_unsigned(ln_sum_e)?) { + Ok(res) + } else { + Err("[RikiddoSigmoidMV] Overflow during calculation: fee * total_asset_balance * \ + ln(sum_i(e^i))") + } + } + + /// Return price P_i(q) for asset q_i in q + fn price( + &self, + _asset_in_question_balance: &Self::FU, + _asset_balances: &[Self::FU], + ) -> Result { + Err("Unimplemented") + } +} + +impl RikiddoMV for RikiddoSigmoidMV +where + FU: FixedUnsigned + LossyFrom> + LossyFrom>, + FS: FixedSigned + + From + + LossyFrom> + + LossyFrom + + LossyFrom> + + PartialOrd, + FS::Bits: Copy + ToFixed + AddAssign + BitOrAssign + ShlAssign, + FE: Sigmoid, + MA: MarketAverage, +{ + /// Clear market data + fn clear(&mut self) { + self.ma_short.clear(); + self.ma_long.clear(); + } + + /// Update market data + /// Returns volume ratio short / long or None + fn update( + &mut self, + volume: &TimestampedVolume, + ) -> Result, &'static str> { + let mas = self.ma_short.update(volume)?; + let mal = self.ma_long.update(volume)?; + + if let Some(mas) = mas { + if let Some(mal) = mal { + if mal != 0u32.to_fixed::() { + return Ok(Some(mas.saturating_div(mal))); + } + }; + }; + + Ok(None) + } +} diff --git a/zrml/rikiddo/src/types/sigmoid_fee.rs b/zrml/rikiddo/src/types/sigmoid_fee.rs index 9cefd62c28..99115aea2a 100644 --- a/zrml/rikiddo/src/types/sigmoid_fee.rs +++ b/zrml/rikiddo/src/types/sigmoid_fee.rs @@ -1,41 +1,72 @@ use crate::{ - constants::{M, N, P}, + constants::{INITIAL_FEE, M, MINIMAL_REVENUE, N, P}, traits::Sigmoid, }; use frame_support::dispatch::{fmt::Debug, Decode, Encode}; use substrate_fixed::{ - traits::{Fixed, FixedSigned, LossyFrom, LossyInto}, + traits::{FixedSigned, LossyFrom, LossyInto}, transcendental::sqrt, - types::{extra::U24, I9F23}, - FixedI32, + types::{ + extra::{U127, U24, U32}, + I9F23, + }, + FixedI128, FixedI32, FixedU32, }; +use super::convert_to_signed; + #[derive(Clone, Debug, Decode, Encode, Eq, PartialEq)] -pub struct FeeSigmoidConfig { - pub m: F, - pub p: F, - pub n: F, +pub struct FeeSigmoidConfig { + pub m: FS, + pub p: FS, + pub n: FS, + pub initial_fee: FS, + pub min_revenue: FS, } -impl>> Default for FeeSigmoidConfig { +impl Default for FeeSigmoidConfig +where + FS: FixedSigned + LossyFrom> + LossyFrom>, +{ fn default() -> Self { - // To avoid a limitation of the generics, the values are hardcoded - // instead of being fetched from constants. - Self { m: M.lossy_into(), p: P.lossy_into(), n: N.lossy_into() } + Self { + m: M.lossy_into(), + p: P.lossy_into(), + n: N.lossy_into(), + // Only case this can panic is, when INITIAL_FEE is >= 1.0 and FS integer bits < 2 + initial_fee: convert_to_signed::, FS>(INITIAL_FEE.lossy_into()).unwrap(), + // Only case this can panic is, when MIN_REVENUE is >= 1.0 and FS integer bits < 2 + min_revenue: convert_to_signed::, FS>(MINIMAL_REVENUE.lossy_into()) + .unwrap(), + } } } #[derive(Clone, Debug, Decode, Default, Encode, Eq, PartialEq)] -pub struct FeeSigmoid>> { - pub config: FeeSigmoidConfig, +pub struct FeeSigmoid +where + FS: FixedSigned + LossyFrom> + LossyFrom>, +{ + pub config: FeeSigmoidConfig, +} + +impl FeeSigmoid +where + FS: FixedSigned + LossyFrom> + LossyFrom>, +{ + pub fn new(config: FeeSigmoidConfig) -> Self { + Self { config } + } } -impl Sigmoid for FeeSigmoid +impl Sigmoid for FeeSigmoid where - F: FixedSigned + LossyFrom> + PartialOrd, + FS: FixedSigned + LossyFrom> + PartialOrd + LossyFrom>, { + type FS = FS; + // z(r) in https://files.kyber.network/DMM-Feb21.pdf - fn calculate(&self, r: F) -> Result { + fn calculate(&self, r: Self::FS) -> Result { let r_minus_n = if let Some(res) = r.checked_sub(self.config.n) { res } else { @@ -61,12 +92,24 @@ where return Err("[FeeSigmoid] Overflow during calculation: p + (r-n)^2"); }; - let denominator = sqrt::(p_plus_r_minus_n_squared)?; + let denominator = sqrt::(p_plus_r_minus_n_squared)?; - let _ = if let Some(res) = numerator.checked_div(denominator) { - return Ok(res); + let sigmoid_result = if let Some(res) = numerator.checked_div(denominator) { + res } else { return Err("[FeeSigmoid] Overflow during calculation: numerator / denominator"); }; + + let result = if let Some(res) = sigmoid_result.checked_add(self.config.initial_fee) { + res + } else { + return Err("[FeeSigmoid] initial_fee + sigmoid_result"); + }; + + if self.config.min_revenue >= result { + return Ok(self.config.min_revenue); + } + + Ok(result) } }