mars-v2-frontend/hooks/useCalculateMaxTradeAmount.tsx
Gustavo Mauricio 1deba2059e
MP-1699: Trade on Margin Account (#52)
* update generated types

* added CRO to token info

* update contract addresses to match latest deployment

* feat: token prices fetched from oracle contract

* trade page initial commit

* trade asset action hook

* extract max swap amount logic into custom hook

* trade component ui adjustments

* trade container min-width and some styling improvements

* trade success message and loading indicator

* normalize naming conventions on trading

* max swap amount formula adjustments

* trade execute msg with borrow. code cleanup

* fix: click max update tokenOut amount. remove wallet from fund denom

* delay token amount decimal conversion. input formatting

* increase hardcoded gas

* renamed max swappable amount hook

* display token prices and market information on landing page

* reset trade amounts when selected account change

* max trade amount cleanup and minor performance optimizations

* fix: liabilities value with 1 hour interest buffer for trade action

* add token symbol to wallet and account labels

* swap trade pairs icon and basic functionality

* remove unnecessary optional chaining. comment adjusted

* refactor useTokenPrices to build query dynamically on tokens data

* extracted trade container and respective functionality into separate file

* fix: properly calculate positions after full swap

* mp-1218: trading using wallet
2022-11-22 10:14:12 +01:00

145 lines
5.0 KiB
TypeScript

import { useCallback, useMemo } from 'react'
import BigNumber from 'bignumber.js'
import useCreditManagerStore from 'stores/useCreditManagerStore'
import useCreditAccountPositions from './useCreditAccountPositions'
import useMarkets from './useMarkets'
import useRedbankBalances from './useRedbankBalances'
import useTokenPrices from './useTokenPrices'
const getApproximateHourlyInterest = (amount: string, borrowAPY: string) => {
const hourlyAPY = BigNumber(borrowAPY).div(24 * 365)
return hourlyAPY.times(amount).toNumber()
}
// max trade amount doesnt consider wallet balance as its not relevant
// the entire token balance within the wallet will always be able to be fully swapped
const useCalculateMaxTradeAmount = (tokenIn: string, tokenOut: string, isMargin: boolean) => {
const selectedAccount = useCreditManagerStore((s) => s.selectedAccount)
const { data: positionsData } = useCreditAccountPositions(selectedAccount ?? '')
const { data: marketsData } = useMarkets()
const { data: tokenPrices } = useTokenPrices()
const { data: redbankBalances } = useRedbankBalances()
const accountAmount = useMemo(() => {
return BigNumber(
positionsData?.coins?.find((coin) => coin.denom === tokenIn)?.amount ?? 0,
).toNumber()
}, [positionsData, tokenIn])
const getTokenValue = useCallback(
(amount: string, denom: string) => {
if (!tokenPrices) return 0
return BigNumber(amount).times(tokenPrices[denom]).toNumber()
},
[tokenPrices],
)
return useMemo(() => {
if (!marketsData || !tokenPrices || !positionsData || !redbankBalances || !tokenIn || !tokenOut)
return 0
const totalWeightedPositions = positionsData.coins.reduce((acc, coin) => {
const tokenWeightedValue = BigNumber(getTokenValue(coin.amount, coin.denom)).times(
Number(marketsData[coin.denom].max_loan_to_value),
)
return tokenWeightedValue.plus(acc).toNumber()
}, 0)
// approximate debt value in an hour timespan to avoid throwing on smart contract level
// due to debt interest being applied
const totalLiabilitiesValue = positionsData.debts.reduce((acc, coin) => {
const estimatedInterestAmount = getApproximateHourlyInterest(
coin.amount,
marketsData[coin.denom].borrow_rate,
)
const tokenDebtValue = BigNumber(getTokenValue(coin.amount, coin.denom)).plus(
estimatedInterestAmount,
)
return tokenDebtValue.plus(acc).toNumber()
}, 0)
const tokenOutLTV = Number(marketsData[tokenOut].max_loan_to_value)
const tokenInLTV = Number(marketsData[tokenIn].max_loan_to_value)
// if the target token ltv higher, the full amount will always be able to be swapped
if (tokenOutLTV < tokenInLTV) {
// in theory, the most you can swap from x to y while keeping an health factor of 1
const maxSwapValue = BigNumber(totalLiabilitiesValue)
.minus(totalWeightedPositions)
.dividedBy(tokenOutLTV - tokenInLTV)
const maxSwapAmount = maxSwapValue.div(tokenPrices[tokenIn]).decimalPlaces(0)
// if the swappable amount is lower than the account amount, any further calculations are irrelevant
if (maxSwapAmount.isLessThanOrEqualTo(accountAmount)) return maxSwapAmount.toNumber()
}
// if margin is disabled, the max swap amount is capped at the account amount
if (!isMargin) {
return accountAmount
}
const estimatedTokenOutAmount = BigNumber(accountAmount).times(
tokenPrices[tokenIn] / tokenPrices[tokenOut],
)
let positionsCoins = [...positionsData.coins]
// if the target token is not in the account, add it to the positions
if (!positionsCoins.find((coin) => coin.denom === tokenOut)) {
positionsCoins.push({
amount: '0',
denom: tokenOut,
})
}
// calculate weighted positions assuming the initial swap is made
const totalWeightedPositionsAfterSwap = positionsCoins
.filter((coin) => coin.denom !== tokenIn)
.reduce((acc, coin) => {
const coinAmount =
coin.denom === tokenOut
? BigNumber(coin.amount).plus(estimatedTokenOutAmount).toString()
: coin.amount
const tokenWeightedValue = BigNumber(getTokenValue(coinAmount, coin.denom)).times(
Number(marketsData[coin.denom].max_loan_to_value),
)
return tokenWeightedValue.plus(acc).toNumber()
}, 0)
const maxBorrowValue =
totalWeightedPositionsAfterSwap === 0
? 0
: BigNumber(totalWeightedPositionsAfterSwap)
.minus(totalLiabilitiesValue)
.dividedBy(1 - tokenOutLTV)
.toNumber()
const maxBorrowAmount = BigNumber(maxBorrowValue).dividedBy(tokenPrices[tokenIn]).toNumber()
return BigNumber(accountAmount).plus(maxBorrowAmount).decimalPlaces(0).toNumber()
}, [
accountAmount,
getTokenValue,
isMargin,
marketsData,
positionsData,
redbankBalances,
tokenIn,
tokenOut,
tokenPrices,
])
}
export default useCalculateMaxTradeAmount