Skip to content

Commit

Permalink
Feat: No maturity date (#1843)
Browse files Browse the repository at this point in the history
* feat: try no matirirty date

* fix: wrongly using notional instead of given price

* Revert "fix: wrongly using notional instead of given price"

This reverts commit f3b5746.

* fix: external pricing with no maturity

* chore: better readability

* chore: simpler test util

* chore: cleaner error message

* fix: tests
  • Loading branch information
mustermeiszer authored May 29, 2024
1 parent a68b1d8 commit a87f4ca
Show file tree
Hide file tree
Showing 8 changed files with 122 additions and 23 deletions.
9 changes: 5 additions & 4 deletions pallets/loans/src/entities/loans.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,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 @@ -260,9 +260,10 @@ 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) => match self.maturity_date() {
Some(maturity) => Ok(now >= maturity.ensure_add(*overdue_secs)?),
None => Ok(false),
},
WriteOffTrigger::PriceOutdated(secs) => match &self.pricing {
ActivePricing::External(pricing) => {
Ok(now >= pricing.last_updated(pool_id).ensure_add(*secs)?)
Expand Down
22 changes: 11 additions & 11 deletions pallets/loans/src/entities/pricing/external.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,31 +156,31 @@ impl<T: Config> ExternalActivePricing<T> {

fn maybe_with_linear_accrual_price(
&self,
maturity: Seconds,
maturity: Option<Seconds>,
price: T::Balance,
price_last_updated: Seconds,
) -> Result<T::Balance, DispatchError> {
if self.info.with_linear_pricing {
if let (Some(maturity), true) = (maturity, self.info.with_linear_pricing) {
if min(price_last_updated, maturity) == maturity {
// We can not have 2 'xs' with different 'y' in a rect.
// That only happens at maturity
return Ok(self.info.notional);
}

Ok(cfg_utils::math::y_coord_in_rect(
return Ok(cfg_utils::math::y_coord_in_rect(
(min(price_last_updated, maturity), price),
(maturity, self.info.notional),
min(T::Time::now(), maturity),
)?)
} else {
Ok(price)
)?);
}

Ok(price)
}

pub fn current_price(
&self,
pool_id: T::PoolId,
maturity: Seconds,
maturity: Option<Seconds>,
) -> Result<T::Balance, DispatchError> {
self.current_price_inner(
maturity,
Expand All @@ -190,7 +190,7 @@ impl<T: Config> ExternalActivePricing<T> {

fn current_price_inner(
&self,
maturity: Seconds,
maturity: Option<Seconds>,
oracle: Option<PriceOf<T>>,
) -> Result<T::Balance, DispatchError> {
if let Some((oracle_price, oracle_provided_at)) = oracle {
Expand All @@ -211,7 +211,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 @@ -229,15 +229,15 @@ 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, PriceOf<T>>,
maturity: Seconds,
maturity: Option<Seconds>,
) -> Result<T::Balance, DispatchError> {
let price = self.current_price_inner(maturity, cache.get(&self.info.price_id).copied())?;
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 @@ -270,3 +270,62 @@ fn no_linear_pricing_either_settlement_or_oracle() {
expected_portfolio(QUANTITY.saturating_mul_int(MARKET_PRICE_VALUE));
});
}

#[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);
});
}
22 changes: 22 additions & 0 deletions pallets/loans/src/tests/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,28 @@ 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 {
pricing: Pricing::Internal(dcf_internal_pricing()),
..base_internal_loan()
}
}

pub fn base_internal_loan() -> LoanInfo<Runtime> {
LoanInfo {
schedule: RepaymentSchedule {
Expand Down
17 changes: 12 additions & 5 deletions pallets/loans/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use parity_scale_codec::{Decode, Encode, MaxEncodedLen};
use scale_info::TypeInfo;
use sp_runtime::{
traits::{EnsureAdd, EnsureAddAssign, EnsureSubAssign},
ArithmeticError,
ArithmeticError, DispatchError,
};

pub mod policy;
Expand Down Expand Up @@ -95,31 +95,38 @@ 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,
}
}

pub fn extends(&mut self, value: Seconds) -> Result<(), ArithmeticError> {
pub fn extends(&mut self, value: Seconds) -> Result<(), DispatchError> {
match self {
Maturity::Fixed { date, extension } => {
date.ensure_add_assign(value)?;
extension.ensure_sub_assign(value)
extension.ensure_sub_assign(value).map_err(Into::into)
}
Maturity::None => Err(DispatchError::Other(
"No maturity date that could be extended.",
)),
}
}
}
Expand Down

0 comments on commit a87f4ca

Please sign in to comment.