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

CU-1m98wce lending docs and fixes #441

Merged
merged 8 commits into from
Jan 6, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frame/composable-traits/jump_model.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
100 changes: 42 additions & 58 deletions frame/composable-traits/src/lending/math.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ impl InterestRateModel {
full_rate: Rate,
target_utilization: Percent,
) -> Option<Self> {
JumpModel::new_model(base_rate, jump_rate, full_rate, target_utilization).map(Self::Jump)
JumpModel::new(base_rate, jump_rate, full_rate, target_utilization).map(Self::Jump)
}

pub fn new_curve_model(base_rate: Rate) -> Option<Self> {
Expand All @@ -95,25 +95,19 @@ impl InterestRateModel {
proportional_parameter: FixedI128,
integral_parameter: FixedI128,
derivative_parameter: FixedI128,
previous_error_value: FixedI128,
previous_integral_term: FixedI128,
previous_interest_rate: FixedU128,
optimal_utilization_ratio: FixedU128,
target_utilization: FixedU128,
) -> Option<Self> {
DynamicPIDControllerModel::new_model(
DynamicPIDControllerModel::new(
proportional_parameter,
integral_parameter,
derivative_parameter,
previous_error_value,
previous_integral_term,
previous_interest_rate,
optimal_utilization_ratio,
target_utilization,
)
.map(Self::DynamicPIDController)
}

pub fn new_double_exponent_model(coefficients: [u8; 16]) -> Option<Self> {
DoubleExponentModel::new_model(coefficients).map(Self::DoubleExponent)
DoubleExponentModel::new(coefficients).map(Self::DoubleExponent)
}

/// Calculates the current supply interest rate
Expand Down Expand Up @@ -162,7 +156,7 @@ impl JumpModel {
pub const MAX_FULL_RATE: Ratio = Ratio::from_inner(500_000_000_000_000_000); // 50%

/// Create a new rate model
pub fn new_model(
pub fn new(
base_rate: Ratio,
jump_rate: Ratio,
full_rate: Ratio,
Expand Down Expand Up @@ -249,7 +243,7 @@ impl InterestRate for CurveModel {
/// https://www.delphidigital.io/reports/dynamic-interest-rate-model-based-on-control-theory/
/// PID Controller (proportional-integral-derivative controller)
/// Error term is calculated as `et = uo - ut`.
/// Propertional term is calculated as `pt = kp * et`.
/// Proportional term is calculated as `pt = kp * et`.
/// Integral term is calculated as `it = it_1 + ki * et`, here `it_1` is previous_integral_term.
/// Derivative term is calculated as `dt = kd * (et - et_1)`. here `et_1` is previous_error_value.
/// Control value is calculated as `ut = pt + it + dt`.
Expand All @@ -259,20 +253,20 @@ impl InterestRate for CurveModel {
#[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))]
#[derive(Encode, Decode, Eq, PartialEq, Copy, Clone, RuntimeDebug, Default, TypeInfo)]
pub struct DynamicPIDControllerModel {
/// proportional_parameter
kp: FixedI128,
/// integral_parameter
ki: FixedI128,
/// derivative_parameter
kd: FixedI128,
/// previous error value
et_1: FixedI128,
/// previous integral term
it_1: FixedI128,
/// previous interest rate
ir_t_1: FixedU128,
/// optimal utilization_ratio
uo: FixedU128,
/// `kp`
proportional_parameter: FixedI128,
/// `ki`
integral_parameter: FixedI128,
/// `kd`
derivative_parameter: FixedI128,
/// `et_1`
previous_error_value: FixedI128,
/// `it_1`
previous_integral_term: FixedI128,
/// `ir_t_1`
previous_interest_rate: FixedU128,
/// uo
target_utilization: FixedU128,
}

impl DynamicPIDControllerModel {
Expand All @@ -281,54 +275,44 @@ impl DynamicPIDControllerModel {
utilization_ratio: FixedU128,
) -> Result<Rate, ArithmeticError> {
// compute error term `et = uo - ut`
let et: i128 = self.uo.into_inner().try_into().unwrap_or(0i128) -
let et: i128 = self.target_utilization.into_inner().try_into().unwrap_or(0i128) -
utilization_ratio.into_inner().try_into().unwrap_or(0i128);
let et: FixedI128 = FixedI128::from_inner(et);
// compute proportional term `pt = kp * et`
let pt = self.kp.checked_mul(&et).ok_or(ArithmeticError::Overflow)?;
let pt = self.proportional_parameter.checked_mul(&et).ok_or(ArithmeticError::Overflow)?;
//compute integral term `it = it_1 + ki * et`
let it = self
.it_1
.checked_add(&self.ki.checked_mul(&et).ok_or(ArithmeticError::Overflow)?)
.previous_integral_term
.checked_add(&self.integral_parameter.checked_mul(&et).ok_or(ArithmeticError::Overflow)?)
.ok_or(ArithmeticError::Overflow)?;
self.it_1 = it;
self.previous_integral_term = it;
// compute derivative term `dt = kd * (et - et_1)`
let dt = self.kd.checked_mul(&(et - self.et_1)).ok_or(ArithmeticError::Overflow)?;
self.et_1 = et;
let dt = self.derivative_parameter.checked_mul(&(et - self.previous_error_value)).ok_or(ArithmeticError::Overflow)?;
dzmitry-lahoda marked this conversation as resolved.
Show resolved Hide resolved
self.previous_error_value = et;

// compute u(t), control value `ut = pt + it + dt`
let ut = pt + it + dt;
// update interest_rate `ir = ir_t_1 + ut`
if ut.is_negative() {
vivekvpandya marked this conversation as resolved.
Show resolved Hide resolved
let ut = ut.neg();
self.ir_t_1 = self
.ir_t_1
.saturating_sub(FixedU128::from_inner(ut.into_inner().try_into().unwrap_or(0u128)));
} else {
self.ir_t_1 = self
.ir_t_1
.saturating_add(FixedU128::from_inner(ut.into_inner().try_into().unwrap_or(0u128)));
}
Ok(self.ir_t_1)
self.previous_interest_rate = self
.previous_interest_rate
.saturating_add(FixedU128::from_inner(ut.into_inner().try_into().unwrap_or(0u128))).max(FixedU128::zero());
Ok(self.previous_interest_rate)
}

pub fn new_model(
pub fn new(
proportional_parameter: FixedI128,
integral_parameter: FixedI128,
derivative_parameter: FixedI128,
previous_error_value: FixedI128,
previous_integral_term: FixedI128,
previous_interest_rate: FixedU128,
optimal_utilization_ratio: FixedU128,
target_utilization: FixedU128,
) -> Option<DynamicPIDControllerModel> {
Some(DynamicPIDControllerModel {
kp: proportional_parameter,
ki: integral_parameter,
kd: derivative_parameter,
et_1: previous_error_value,
it_1: previous_integral_term,
ir_t_1: previous_interest_rate,
uo: optimal_utilization_ratio,
proportional_parameter,
integral_parameter,
derivative_parameter,
previous_error_value : <_>::zero(),
previous_integral_term : <_>::zero(),
previous_interest_rate : <_>::zero(),
target_utilization,
})
}
}
Expand Down Expand Up @@ -361,7 +345,7 @@ pub struct DoubleExponentModel {

impl DoubleExponentModel {
/// Create a double exponent model
pub fn new_model(coefficients: [u8; 16]) -> Option<Self> {
pub fn new(coefficients: [u8; 16]) -> Option<Self> {
let sum_of_coefficients = coefficients.iter().fold(0u16, |acc, &c| acc + c as u16);
if sum_of_coefficients == EXPECTED_COEFFICIENTS_SUM {
return Some(DoubleExponentModel { coefficients })
Expand Down
31 changes: 14 additions & 17 deletions frame/composable-traits/src/lending/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ fn init_jump_model_works() {
let full_rate = Rate::saturating_from_rational(32, 100);
let target_utilization = Percent::from_percent(80);
assert_eq!(
JumpModel::new_model(base_rate, jump_rate, full_rate, target_utilization).unwrap(),
JumpModel::new(base_rate, jump_rate, full_rate, target_utilization).unwrap(),
JumpModel {
base_rate: Rate::from_inner(20_000_000_000_000_000),
jump_rate: Rate::from_inner(100_000_000_000_000_000),
Expand All @@ -34,7 +34,7 @@ fn get_borrow_rate_works() {
let full_rate = Rate::saturating_from_rational(32, 100);
let target_utilization = Percent::from_percent(80);
let mut jump_model =
JumpModel::new_model(base_rate, jump_rate, full_rate, target_utilization).unwrap();
JumpModel::new(base_rate, jump_rate, full_rate, target_utilization).unwrap();
// normal rate
let mut cash: u128 = 500;
let borrows: u128 = 1000;
Expand Down Expand Up @@ -116,7 +116,7 @@ fn valid_jump_model() -> impl Strategy<Value = JumpModelStrategy> {

#[test]
fn test_empty_drained_market() {
let mut jump_model = JumpModel::new_model(
let mut jump_model = JumpModel::new(
FixedU128::from_float(0.010000000000000000),
FixedU128::from_float(0.110000000000000000),
FixedU128::from_float(0.310000000000000000),
Expand All @@ -132,7 +132,7 @@ fn test_empty_drained_market() {

#[test]
fn test_slope() {
let mut jump_model = JumpModel::new_model(
let mut jump_model = JumpModel::new(
FixedU128::from_float(0.010000000000000000),
FixedU128::from_float(0.110000000000000000),
FixedU128::from_float(0.310000000000000000),
Expand Down Expand Up @@ -167,7 +167,7 @@ fn proptest_jump_model() {
let full_rate = strategy.full_percentage;
let target_utilization = strategy.target_utilization;
let mut jump_model =
JumpModel::new_model(base_rate, jump_rate, full_rate, target_utilization).unwrap();
JumpModel::new(base_rate, jump_rate, full_rate, target_utilization).unwrap();

let utilization = Percent::from_percent(utilization);
let borrow_rate =
Expand All @@ -192,7 +192,7 @@ fn proptest_jump_model_rate() {
let utilization_1 = Percent::from_percent(previous);
let utilization_2 = Percent::from_percent(next);
let optimal = Percent::from_percent(optimal);
let mut model = JumpModel::new_model(base_rate, jump_rate, full_rate, optimal)
let mut model = JumpModel::new(base_rate, jump_rate, full_rate, optimal)
.expect("model should be defined");
let rate_1 = model.get_borrow_rate(utilization_1);
let rate_2 = model.get_borrow_rate(utilization_2);
Expand All @@ -212,7 +212,7 @@ fn jump_model_plotter() {
let jump_rate = Rate::saturating_from_rational(10, 100);
let full_rate = Rate::saturating_from_rational(32, 100);
let optimal = Percent::from_percent(80);
let mut model = JumpModel::new_model(base_rate, jump_rate, full_rate, optimal).unwrap();
let mut model = JumpModel::new(base_rate, jump_rate, full_rate, optimal).unwrap();

let area = BitMapBackend::new("./jump_model.png", (1024, 768)).into_drawing_area();
area.fill(&WHITE).unwrap();
Expand Down Expand Up @@ -275,15 +275,12 @@ fn curve_model_plotter() {
#[test]
fn dynamic_pid_model_plotter() {
use plotters::prelude::*;
let kp = FixedI128::saturating_from_rational(600, 100);
let ki = FixedI128::saturating_from_rational(200, 100);
let kd = FixedI128::saturating_from_rational(1275, 100);
let et_1 = FixedI128::from_inner(0i128);
let it_1 = FixedI128::from_inner(0i128);
let ir_t_1 = FixedU128::saturating_from_rational(500, 100);
let uo = FixedU128::saturating_from_rational(80, 100);
let proportional_parameter = FixedI128::saturating_from_integer(5);
let integral_parameter = FixedI128::saturating_from_integer(0);
let derivative_parameter = FixedI128::saturating_from_integer(0);
let target_utilization = FixedU128::saturating_from_rational(80, 100);
let mut model =
DynamicPIDControllerModel::new_model(kp, ki, kd, et_1, it_1, ir_t_1, uo).unwrap();
DynamicPIDControllerModel::new(proportional_parameter, integral_parameter, derivative_parameter, target_utilization).unwrap();

let area = BitMapBackend::new("./dynamic_pid_model_plotter.png", (1024, 768)).into_drawing_area();
area.fill(&WHITE).unwrap();
Expand All @@ -295,7 +292,7 @@ fn dynamic_pid_model_plotter() {
.unwrap();
chart
.configure_mesh()
.x_desc("Utilization ratio %")
.x_desc("Time")
.y_desc("Borrow rate %")
.draw().unwrap();
chart
Expand Down Expand Up @@ -326,7 +323,7 @@ fn dynamic_pid_model_plotter() {
fn double_exponents_model_plotter() {
use plotters::prelude::*;
let coefficients: [u8; 16] = [10, 40, 50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
let mut model = DoubleExponentModel::new_model(coefficients).unwrap();
let mut model = DoubleExponentModel::new(coefficients).unwrap();
let area = BitMapBackend::new("./double_exponents_model_plotter.png", (1024, 768)).into_drawing_area();
area.fill(&WHITE).unwrap();

Expand Down