1
0
mirror of https://github.com/oceanprotocol/market.git synced 2024-12-02 05:57:29 +01:00

Add price impact indicator to the trade component (#765)

* WIP

* calculate price impact by subtracting tokens fiat values

* get and use spotPrice for price impact calculation, use Decimal

* set impact to 0 if input and output values are undefined

* move price impact to a new component

* turn price impact value color to red if grater than 5

* add tooltip to price impact and slippage

* removed fiat price

* change formula

* remove console.log

* don't block add liquidity button

* typos

* proper text alignment

Co-authored-by: mihaisc <mihai@oceanprotocol.com>
Co-authored-by: Matthias Kretschmann <m@kretschmann.io>
This commit is contained in:
Norbi 2021-08-18 12:46:27 +03:00 committed by GitHub
parent dc17aa0101
commit e500772d21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 183 additions and 19 deletions

View File

@ -367,7 +367,7 @@ export default function Pool(): ReactElement {
style="primary" style="primary"
size="small" size="small"
onClick={() => setShowAdd(true)} onClick={() => setShowAdd(true)}
disabled={isInPurgatory || !isAssetNetwork} disabled={isInPurgatory}
> >
Add Liquidity Add Liquidity
</Button> </Button>

View File

@ -0,0 +1,24 @@
.priceImpact {
font-size: var(--font-size-small);
border-top: 1px solid var(--border-color);
margin-left: -2rem;
margin-right: -2rem;
padding: calc(var(--spacer) / 4) var(--spacer);
color: var(--color-secondary);
display: grid;
grid-template-columns: 1fr 1fr;
gap: calc(var(--spacer) / 3);
}
.priceImpact strong {
font-weight: var(--font-weight-base);
text-align: right;
}
.alert {
color: var(--brand-alert-red);
}
.number {
font-weight: var(--font-weight-bold);
}

View File

@ -0,0 +1,65 @@
import React, { ReactElement, useEffect, useState } from 'react'
import Decimal from 'decimal.js'
import Tooltip from '../../../atoms/Tooltip'
import styles from './PriceImpact.module.css'
export default function PriceImpact({
totalValue,
tokenAmount,
spotPrice
}: {
/// how much the user actually pays (doesn't matter witch token it is)
totalValue: string
/// how many tokens the user trades (doesn't matter witch token it is)
tokenAmount: string
/// the spot price of the traded token (doesn't matter witch token it is))
spotPrice: string
}): ReactElement {
const [priceImpact, setPriceImpact] = useState<string>('0')
async function getPriceImpact(
totalValue: string,
tokenAmount: string,
spotPrice: string
) {
const dtotalValue = new Decimal(totalValue)
const dTokenAmount = new Decimal(tokenAmount)
const dSpotPrice = new Decimal(spotPrice)
let priceImpact = Decimal.abs(
dtotalValue.div(dTokenAmount.times(dSpotPrice)).minus(1)
).mul(100)
if (priceImpact.isNaN()) priceImpact = new Decimal(0)
return priceImpact.toDecimalPlaces(2, Decimal.ROUND_DOWN)
}
useEffect(() => {
if (!totalValue || !tokenAmount || !spotPrice) {
setPriceImpact('0')
return
}
async function init() {
const newPriceImpact = await getPriceImpact(
totalValue,
tokenAmount,
spotPrice
)
setPriceImpact(newPriceImpact.toString())
}
init()
}, [totalValue, tokenAmount, spotPrice])
return (
<div className={styles.priceImpact}>
<strong>Price impact</strong>
<div>
<span
className={`${styles.number} ${
parseInt(priceImpact) > 5 && styles.alert
}`}
>{`${priceImpact}%`}</span>
<Tooltip content="The difference between the market price and estimated price due to trade size." />
</div>
</div>
)
}

View File

@ -4,13 +4,15 @@
margin-left: -2rem; margin-left: -2rem;
margin-right: -2rem; margin-right: -2rem;
padding: calc(var(--spacer) / 4) var(--spacer); padding: calc(var(--spacer) / 4) var(--spacer);
text-align: center;
color: var(--color-secondary); color: var(--color-secondary);
display: grid;
grid-template-columns: 1fr 1fr;
gap: calc(var(--spacer) / 3);
} }
.slippage strong { .slippage strong {
font-weight: var(--font-weight-base); font-weight: var(--font-weight-base);
color: var(--font-color-heading); text-align: right;
} }
.title { .title {
@ -25,6 +27,4 @@
.slippage select { .slippage select {
width: fit-content; width: fit-content;
display: inline-block; display: inline-block;
margin-left: calc(var(--spacer) / 4);
margin-right: calc(var(--spacer) / 4);
} }

View File

@ -1,7 +1,8 @@
import { FormikContextType, useFormikContext } from 'formik' import { FormikContextType, useFormikContext } from 'formik'
import React, { ChangeEvent, ReactElement, useEffect, useState } from 'react' import React, { ChangeEvent, ReactElement } from 'react'
import { FormTradeData, slippagePresets } from '../../../../models/FormTrade' import { FormTradeData, slippagePresets } from '../../../../models/FormTrade'
import InputElement from '../../../atoms/Input/InputElement' import InputElement from '../../../atoms/Input/InputElement'
import Tooltip from '../../../atoms/Tooltip'
import styles from './Slippage.module.css' import styles from './Slippage.module.css'
export default function Slippage(): ReactElement { export default function Slippage(): ReactElement {
@ -14,9 +15,9 @@ export default function Slippage(): ReactElement {
} }
return ( return (
<> <div className={styles.slippage}>
<div className={styles.slippage}> <strong>Slippage Tolerance</strong>
<strong>Expected price impact</strong> <div>
<InputElement <InputElement
name="slippage" name="slippage"
type="select" type="select"
@ -27,7 +28,8 @@ export default function Slippage(): ReactElement {
value={values.slippage} value={values.slippage}
onChange={handleChange} onChange={handleChange}
/> />
<Tooltip content="Your transaction will revert if the price changes unfavorably by more than this percentage." />
</div> </div>
</> </div>
) )
} }

View File

@ -10,6 +10,7 @@ import Output from './Output'
import Slippage from './Slippage' import Slippage from './Slippage'
import { FormTradeData, TradeItem } from '../../../../models/FormTrade' import { FormTradeData, TradeItem } from '../../../../models/FormTrade'
import { useOcean } from '../../../../providers/Ocean' import { useOcean } from '../../../../providers/Ocean'
import PriceImpact from './PriceImpact'
export default function Swap({ export default function Swap({
ddo, ddo,
@ -47,6 +48,11 @@ export default function Swap({
validateForm validateForm
}: FormikContextType<FormTradeData> = useFormikContext() }: FormikContextType<FormTradeData> = useFormikContext()
/// Values used for calculation of price impact
const [spotPrice, setSpotPrice] = useState<string>()
const [totalValue, setTotalValue] = useState<string>()
const [tokenAmount, setTokenAmount] = useState<string>()
///
useEffect(() => { useEffect(() => {
if (!ddo || !balance || !values || !price) return if (!ddo || !balance || !values || !price) return
@ -106,16 +112,63 @@ export default function Swap({
} }
const handleValueChange = async (name: string, value: number) => { const handleValueChange = async (name: string, value: number) => {
const newValue = let tokenIn = ''
name === 'ocean' let tokenOut = ''
? values.type === 'sell' let newValue
? await ocean.pool.getDTNeeded(price.address, value.toString())
: await ocean.pool.getDTReceived(price.address, value.toString())
: values.type === 'sell'
? await ocean.pool.getOceanReceived(price.address, value.toString())
: await ocean.pool.getOceanNeeded(price.address, value.toString())
setFieldValue(name === 'ocean' ? 'datatoken' : 'ocean', newValue) if (name === 'ocean') {
if (values.type === 'sell') {
newValue = await ocean.pool.getDTNeeded(price.address, value.toString())
setTotalValue(newValue)
setTokenAmount(value.toString())
tokenIn = ddo.dataToken
tokenOut = ocean.pool.oceanAddress
} else {
newValue = await ocean.pool.getDTReceived(
price.address,
value.toString()
)
setTotalValue(value.toString())
setTokenAmount(newValue)
tokenIn = ocean.pool.oceanAddress
tokenOut = ddo.dataToken
}
} else {
if (values.type === 'sell') {
newValue = await ocean.pool.getOceanReceived(
price.address,
value.toString()
)
setTotalValue(value.toString())
setTokenAmount(newValue)
tokenIn = ddo.dataToken
tokenOut = ocean.pool.oceanAddress
} else {
newValue = await ocean.pool.getOceanNeeded(
price.address,
value.toString()
)
setTotalValue(newValue)
setTokenAmount(value.toString())
tokenIn = ocean.pool.oceanAddress
tokenOut = ddo.dataToken
}
}
await setFieldValue(name === 'ocean' ? 'datatoken' : 'ocean', newValue)
const spotPrice = await ocean.pool.getSpotPrice(
price.address,
tokenIn,
tokenOut
)
setSpotPrice(spotPrice)
validateForm() validateForm()
} }
@ -139,6 +192,11 @@ export default function Swap({
<Output dtSymbol={dtItem.token} poolAddress={price?.address} /> <Output dtSymbol={dtItem.token} poolAddress={price?.address} />
<PriceImpact
totalValue={totalValue}
tokenAmount={tokenAmount}
spotPrice={spotPrice}
/>
<Slippage /> <Slippage />
</div> </div>
) )

View File

@ -410,6 +410,21 @@ export async function getPrice(asset: DDO): Promise<BestPrice> {
return bestPrice return bestPrice
} }
export async function getSpotPrice(asset: DDO): Promise<number> {
const poolVariables = {
datatokenAddress: asset?.dataToken.toLowerCase()
}
const queryContext = getQueryContext(Number(asset.chainId))
const poolPriceResponse: OperationResult<AssetsPoolPrice> = await fetchData(
AssetPoolPriceQuerry,
poolVariables,
queryContext
)
return poolPriceResponse.data.pools[0].spotPrice
}
export async function getAssetsBestPrices( export async function getAssetsBestPrices(
assets: DDO[] assets: DDO[]
): Promise<AssetListPrices[]> { ): Promise<AssetListPrices[]> {