Skip to content

Conversation

@da-kami
Copy link
Member

@da-kami da-kami commented Aug 11, 2021

Lender now includes the loan calculations and the validation of the loan.
I clicked it through and it looks sane :)

To be done in follow ups:

  • Send all amounts in sats and do conversion on UI side (did not tackle all amounts yet)
  • Validation step for the loan term
  • Upgrade the UI so the borrower can select values

@da-kami da-kami changed the title Upgrade bobtimus baru Upgrade Botimus to handle loan calc and validation Aug 11, 2021
///
/// We return the range of possible loan terms to the borrower.
/// The borrower can then request a loan using parameters that are within our terms.
pub async fn handle_loan_offer_request(&mut self) -> Result<LoanOffer> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does not look like it needs to be async anymore.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a lint you can enable to catch these automatically: https://rust-lang.github.io/rust-clippy/master/index.html#unused_async

};
use serde::{Deserialize, Serialize};
use std::{convert::TryFrom, fmt::Debug};
use std::{convert::TryFrom, fmt, fmt::Debug};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tend to always import just the fmt module and refer to everything else from there. Makes it more consistent and reduces the number of imports.

The thing is, you will always need fmt::Fromatter and fmt::Result as well.

/// Handle the borrower's loan request in which she puts up L-BTC as
/// collateral and we lend L-USDt to her which she will have to
/// repay in the future.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly to commit messages, idiomatic rustdoc has a subject line with more fine-grained documentation in another paragraph: https://doc.rust-lang.org/rustdoc/how-to-write-documentation.html#documenting-components

&loan_offer.terms,
&loan_offer.collateralizations,
loan_offer.base_interest_rate,
)?;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These might be worth extending with .context so you know which part failed.

Comment on lines 29 to 32
let [timestamp, setTimestamp] = useState(Math.floor(Date.now() / 1000));
useInterval(() => {
reloadBlockHeight();
setTimestamp(Math.floor(Date.now() / 1000));
}, 6000); // 1 min
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could extract this into a useTime hook given that you are using it two times.

Comment on lines +103 to +188
for collateralization in collateralization_thresholds {
if borrower_collateralization >= collateralization.collateralization {
collateralization_interest_mod = collateralization.interest_mod;
continue;
}
break;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand the logic here :) Can you explain this please?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall idea: The lender can define different interest rates based on term and collateralization. The LoanOffer contains term and collateralization thresholds that may modify the interest rate of the loan. For collateralizations the assumption is that the modifier would usually be -x (if higher collateraliztion then lower interest rate), for the term it would be +x (if longer term then higher interest rate).

This piece of code: The modifier is set inside the if. As long as the collateralization falls into a higher threshold we keep looping. If the threshold is not higher we stop and use the modifier that was last set.

Note that for the term this has to be more restrictive - I will add a check to the loan acceptance criteria for that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel dumb and don't understand it 😔

pub struct Term {
pub days: u32,
/// Interest to be added on top of the base interest rate for this term
pub interest_mod: Decimal,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this needed for?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use rust_decimal_macros::dec;

#[test]
fn test_calculate_interest_rate() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I honestly cannot follow your tests.
Given that these calculations are critical can we have some code comments explaining what is happening?

Copy link
Member Author

@da-kami da-kami Aug 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not exactly sure what the problem is. From other comments I think there is confusion with the logic of making the interest rate configurable on top of loan term and collateralization.

Additionally, is it problematic that I splitted the calculation into small, testable functions?
At the moment there are tests that make sure that each calculation step is sane - but I did not add tests for the complete loan taking (yet).

I am not sure if it was the best idea to split the calculation into tiny chunks - I can combine more things again and we can write tests that cover bigger pieces of code - would that help or would you just like to have another bigger test on top?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it could be that the confusing part is how the tests are structured.

I have a hard time forming the code here into logic without grabbing another piece of paper and writing it out which makes it hard for me personally to see if the logic is sound.

What I would like to see is something like this (independent of this particular test):

// given: 

LTV: 80%
exchange rate start: 10,000 
interest_rate: 5%p.a.
loan duration: 30 days
principal amount: 10,000
overcollateralization_percent: 200%
overcollateralization_btc: 3

// when
do_some_math()

// then
collateralization_thresholds: X,
interest: Y, 
...

Comment on lines +391 to +477
rate: Rate {
ask: Default::default(),
bid: Default::default(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might still be useful to state it here so that one does not need to do the math on how the collateral_amount was chosen.

As a reference, that's what I came up with:

// collateral_amount / collateralization_percent = value of principal_amount

0.3675 / 1.4 = 0.2625

// so

0.2625 BTC = 10,000

// which means, 

10,000 * 1/0.2625 = BTC price 

rate = 38095 USDT/BTC

now with this and the given max_ltv of 0.75 I get:

initial_price * LTV = liquidation_threshold
38095 * 0.75 = 28571.25

I ignored the 0.05% interest rate but the value seems to be close to what you have here.

}));
}

if terms.iter().find(|a| a.days == request_term).is_none() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clippy is not complaining?

You could also .any here (but not that important):

    if !terms.iter().any(|a| a.days == request_term) {
        return Ok(Err(LoanValidationError::TermNotAllowed {
            term: request_term,
        }));
    }

Comment on lines +388 to +414
let min_collateralization = sorted_collateralizations
.first()
.context("Unable to determine minimum collateralization")?
.collateralization;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could get rid of this error/context handling here if you'd do :

let mut sorted_collateralizations = collateralizations.clone();
    sorted_collateralizations.sort_by(|a, b| a.collateralization.cmp(&b.collateralization));
    if let Some(Collateralization {
        collateralization, ..
    }) = sorted_collateralizations.first()
...

da-kami and others added 8 commits August 14, 2021 15:31
- Upgrade to baru 0.3 that allows to do the calculation of repay_amount, interest_rate (...) in waves
- Loan calculation upon loan request, using static "current" loan offer
- Laon validation upon loan request
- Adapt the term and collateralization model of the loan offer and loan request to enable calculations (UI not extended yet but functional)
Blockheight was still used in extension instead of UNIX timestamp.
It was previously accidentally not adapted because the term value was still hardcoded.
Since we don't need the block-height anywhere else it was removed.
Includes a test for the overall calculation.
If we assume that the loan is paid back immediately the lender has to be covered for selling the asset upon repayment.
Since the lender would sell the USDT at the current bid rate, that is what the collateral (covering the USDT repayment amount) should reflect.
Strict term validation, if the loan term given in the request is not specified (in days) in the current offer the loan is rejected.
Validate that the collateralization rate of a loan request is above the specified minimum collateralization threshold.
There is not check for a maximum (the user can over-collateralize indefinitely).

This commit includes some test refactoring to be able to specify tests faster.
The liquidation price has to reflect the over-collaterization of the borrower as well as the risk hunger of the lender.
The lender's maxiumum LTV ratio has to be used for reflecting the risk hunger.
The borrower is forced to over-collateralize heavily to be on the safe side.
Signed-off-by: Philipp Hoenisch <philipp@hoenisch.at>
@bonomat
Copy link
Member

bonomat commented Aug 14, 2021

I see one issue (somehow related but not a blocker): in the UI we always show the principal amount instead of the repayment amount. Unfortunately it does not seem to be possible to get hold of this number from what is available in

pub async fn extract_loan(

Copy link
Member

@bonomat bonomat left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests run locally. One issue detected and recorded in #242

@mergify mergify bot merged commit c67ba51 into master Aug 14, 2021
@mergify mergify bot deleted the upgrade-bobtimus-baru branch August 14, 2021 06:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants