Skip to content

Commit

Permalink
feat: try no matirirty date
Browse files Browse the repository at this point in the history
  • Loading branch information
mustermeiszer committed May 24, 2024
1 parent 5fc0465 commit 88939a5
Show file tree
Hide file tree
Showing 8 changed files with 133 additions and 14 deletions.
10 changes: 6 additions & 4 deletions pallets/loans/src/entities/loans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ impl<T: Config> ActiveLoan<T> {
&self.borrower
}

pub fn maturity_date(&self) -> Seconds {
pub fn maturity_date(&self) -> Option<Seconds> {
self.schedule.maturity.date()
}

Expand Down Expand Up @@ -259,9 +259,11 @@ impl<T: Config> ActiveLoan<T> {
) -> Result<bool, DispatchError> {
let now = T::Time::now();
match trigger {
WriteOffTrigger::PrincipalOverdue(overdue_secs) => {
Ok(now >= self.maturity_date().ensure_add(*overdue_secs)?)
}
WriteOffTrigger::PrincipalOverdue(overdue_secs) => Ok(now
> self
.maturity_date()
.unwrap_or(T::Time::now())
.ensure_add(*overdue_secs)?),
WriteOffTrigger::PriceOutdated(secs) => match &self.pricing {
ActivePricing::External(pricing) => {
Ok(now >= pricing.last_updated(pool_id).ensure_add(*secs)?)
Expand Down
15 changes: 10 additions & 5 deletions pallets/loans/src/entities/pricing/external.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,10 @@ impl<T: Config> ExternalActivePricing<T> {
pub fn current_price(
&self,
pool_id: T::PoolId,
maturity: Seconds,
maturity: Option<Seconds>,
) -> Result<T::Balance, DispatchError> {
let maturity = maturity.unwrap_or(T::Time::now());

Ok(match T::PriceRegistry::get(&self.info.price_id, &pool_id) {
Ok(data) => data.0,
Err(_) => self.linear_accrual_price(maturity)?,
Expand All @@ -172,7 +174,7 @@ impl<T: Config> ExternalActivePricing<T> {
pub fn outstanding_principal(
&self,
pool_id: T::PoolId,
maturity: Seconds,
maturity: Option<Seconds>,
) -> Result<T::Balance, DispatchError> {
let price = self.current_price(pool_id, maturity)?;
Ok(self.outstanding_quantity.ensure_mul_int(price)?)
Expand All @@ -190,19 +192,22 @@ impl<T: Config> ExternalActivePricing<T> {
pub fn present_value(
&self,
pool_id: T::PoolId,
maturity: Seconds,
maturity: Option<Seconds>,
) -> Result<T::Balance, DispatchError> {
self.outstanding_principal(pool_id, maturity)
}

pub fn present_value_cached(
&self,
cache: &BTreeMap<T::PriceId, T::Balance>,
maturity: Seconds,
maturity: Option<Seconds>,
) -> Result<T::Balance, DispatchError> {
let price = match cache.get(&self.info.price_id) {
Some(data) => *data,
None => self.linear_accrual_price(maturity)?,
None => {
let maturity = maturity.unwrap_or(T::Time::now());
self.linear_accrual_price(maturity)?
}
};
Ok(self.outstanding_quantity.ensure_mul_int(price)?)
}
Expand Down
9 changes: 6 additions & 3 deletions pallets/loans/src/entities/pricing/internal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,13 @@ impl<T: Config> InternalActivePricing<T> {
&self,
debt: T::Balance,
origination_date: Seconds,
maturity_date: Seconds,
maturity_date: Option<Seconds>,
) -> Result<T::Balance, DispatchError> {
match &self.info.valuation_method {
ValuationMethod::DiscountedCashFlow(dcf) => {
let maturity_date =
maturity_date.ok_or(Error::<T>::MaturityDateNeededForValuationMethod)?;

let now = T::Time::now();
Ok(dcf.compute_present_value(
debt,
Expand All @@ -110,7 +113,7 @@ impl<T: Config> InternalActivePricing<T> {
pub fn present_value(
&self,
origination_date: Seconds,
maturity_date: Seconds,
maturity_date: Option<Seconds>,
) -> Result<T::Balance, DispatchError> {
let debt = self.interest.current_debt()?;
self.compute_present_value(debt, origination_date, maturity_date)
Expand All @@ -120,7 +123,7 @@ impl<T: Config> InternalActivePricing<T> {
&self,
cache: &Rates,
origination_date: Seconds,
maturity_date: Seconds,
maturity_date: Option<Seconds>,
) -> Result<T::Balance, DispatchError>
where
Rates: RateCollection<T::Rate, T::Balance, T::Balance>,
Expand Down
4 changes: 4 additions & 0 deletions pallets/loans/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,10 @@ pub mod pallet {
TransferDebtToSameLoan,
/// Emits when debt is transfered with different repaid/borrow amounts
TransferDebtAmountMismatched,
/// Emits when the loan has no maturity date set, but the valuation
/// method needs one. Making valuation and maturity settings
/// incompatible.
MaturityDateNeededForValuationMethod,
}

impl<T> From<CreateLoanError> for Error<T> {
Expand Down
3 changes: 3 additions & 0 deletions pallets/loans/src/tests/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ pub const POOL_OTHER_ACCOUNT: AccountId = 100;

pub const COLLATERAL_VALUE: Balance = 10000;
pub const DEFAULT_INTEREST_RATE: f64 = 0.5;
pub const DEFAULT_DISCOUNT_RATE: f64 = 0.02;
pub const DEFAULT_PROBABILITY_OF_DEFAULT: f64 = 0.1;
pub const DEFAULT_LOSS_GIVEN_DEFAULT: f64 = 0.5;
pub const POLICY_PERCENTAGE: f64 = 0.5;
pub const POLICY_PENALTY: f64 = 0.5;
pub const REGISTER_PRICE_ID: PriceId = 42;
Expand Down
59 changes: 59 additions & 0 deletions pallets/loans/src/tests/portfolio_valuation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,3 +218,62 @@ fn empty_portfolio_with_current_timestamp() {
);
});
}

#[test]
fn internal_dcf_with_no_maturity() {
new_test_ext().execute_with(|| {
let mut internal = util::dcf_internal_loan();
internal.schedule.maturity = Maturity::None;

let loan_id = util::create_loan(LoanInfo {
collateral: ASSET_BA,
..internal
});

MockPools::mock_withdraw(|_, _, _| Ok(()));

assert_noop!(
Loans::borrow(
RuntimeOrigin::signed(util::borrower(loan_id)),
POOL_A,
loan_id,
PrincipalInput::Internal(COLLATERAL_VALUE),
),
Error::<Runtime>::MaturityDateNeededForValuationMethod
);
});
}

#[test]
fn internal_oustanding_debt_with_no_maturity() {
new_test_ext().execute_with(|| {
let mut internal = util::base_internal_loan();
internal.schedule.maturity = Maturity::None;

let loan_id = util::create_loan(LoanInfo {
collateral: ASSET_BA,
..internal
});
util::borrow_loan(loan_id, PrincipalInput::Internal(COLLATERAL_VALUE));

config_mocks();
let pv = util::current_loan_pv(loan_id);
update_portfolio();
expected_portfolio(pv);

advance_time(YEAR);

update_portfolio();
expected_portfolio(
Rate::from_float(util::interest_for(DEFAULT_INTEREST_RATE, YEAR))
.checked_mul_int(COLLATERAL_VALUE)
.unwrap(),
);

util::repay_loan(loan_id, PrincipalInput::Internal(COLLATERAL_VALUE));

config_mocks();
update_portfolio();
expected_portfolio(0);
});
}
38 changes: 38 additions & 0 deletions pallets/loans/src/tests/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,44 @@ pub fn base_internal_pricing() -> InternalPricing<Runtime> {
}
}

pub fn dcf_internal_pricing() -> InternalPricing<Runtime> {
InternalPricing {
collateral_value: COLLATERAL_VALUE,
max_borrow_amount: util::total_borrowed_rate(1.0),
valuation_method: ValuationMethod::DiscountedCashFlow(DiscountedCashFlow {
probability_of_default: Rate::from_float(DEFAULT_PROBABILITY_OF_DEFAULT),
loss_given_default: Rate::from_float(DEFAULT_LOSS_GIVEN_DEFAULT),
discount_rate: InterestRate::Fixed {
rate_per_year: Rate::from_float(DEFAULT_DISCOUNT_RATE),
compounding: CompoundingSchedule::Secondly,
},
}),
}
}

pub fn dcf_internal_loan() -> LoanInfo<Runtime> {
LoanInfo {
schedule: RepaymentSchedule {
maturity: Maturity::Fixed {
date: (now() + YEAR).as_secs(),
extension: (YEAR / 2).as_secs(),
},
interest_payments: InterestPayments::None,
pay_down_schedule: PayDownSchedule::None,
},
interest_rate: InterestRate::Fixed {
rate_per_year: Rate::from_float(DEFAULT_INTEREST_RATE),
compounding: CompoundingSchedule::Secondly,
},
collateral: ASSET_AA,
pricing: Pricing::Internal(dcf_internal_pricing()),
restrictions: LoanRestrictions {
borrows: BorrowRestrictions::NotWrittenOff,
repayments: RepayRestrictions::None,
},
}
}

pub fn base_internal_loan() -> LoanInfo<Runtime> {
LoanInfo {
schedule: RepaymentSchedule {
Expand Down
9 changes: 7 additions & 2 deletions pallets/loans/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,22 +95,26 @@ pub enum Maturity {
/// Extension in secs, without special permissions
extension: Seconds,
},
/// No Maturity date
None,
}

impl Maturity {
pub fn fixed(date: Seconds) -> Self {
Self::Fixed { date, extension: 0 }
}

pub fn date(&self) -> Seconds {
pub fn date(&self) -> Option<Seconds> {
match self {
Maturity::Fixed { date, .. } => *date,
Maturity::Fixed { date, .. } => Some(*date),
Maturity::None => None,
}
}

pub fn is_valid(&self, now: Seconds) -> bool {
match self {
Maturity::Fixed { date, .. } => *date > now,
Maturity::None => true,
}
}

Expand All @@ -120,6 +124,7 @@ impl Maturity {
date.ensure_add_assign(value)?;
extension.ensure_sub_assign(value)
}
Maturity::None => Err(ArithmeticError::Overflow),
}
}
}
Expand Down

0 comments on commit 88939a5

Please sign in to comment.