-
Notifications
You must be signed in to change notification settings - Fork 7
Upgrade Botimus to handle loan calc and validation #233
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
Conversation
| /// | ||
| /// 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> { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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}; |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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
bobtimus/src/lib.rs
Outdated
| &loan_offer.terms, | ||
| &loan_offer.collateralizations, | ||
| loan_offer.base_interest_rate, | ||
| )?; |
There was a problem hiding this comment.
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.
| let [timestamp, setTimestamp] = useState(Math.floor(Date.now() / 1000)); | ||
| useInterval(() => { | ||
| reloadBlockHeight(); | ||
| setTimestamp(Math.floor(Date.now() / 1000)); | ||
| }, 6000); // 1 min |
There was a problem hiding this comment.
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.
| for collateralization in collateralization_thresholds { | ||
| if borrower_collateralization >= collateralization.collateralization { | ||
| collateralization_interest_mod = collateralization.interest_mod; | ||
| continue; | ||
| } | ||
| break; | ||
| } |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
see #233 (comment)
| use rust_decimal_macros::dec; | ||
|
|
||
| #[test] | ||
| fn test_calculate_interest_rate() { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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,
...
| rate: Rate { | ||
| ask: Default::default(), | ||
| bid: Default::default(), |
There was a problem hiding this comment.
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.
bobtimus/src/loan.rs
Outdated
| })); | ||
| } | ||
|
|
||
| if terms.iter().find(|a| a.days == request_term).is_none() { |
There was a problem hiding this comment.
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,
}));
}
| let min_collateralization = sorted_collateralizations | ||
| .first() | ||
| .context("Unable to determine minimum collateralization")? | ||
| .collateralization; |
There was a problem hiding this comment.
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()
...
- 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>
|
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
|
fa23aeb to
1cc5d7e
Compare
bonomat
left a comment
There was a problem hiding this 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
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: