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:
parent
dc17aa0101
commit
e500772d21
@ -367,7 +367,7 @@ export default function Pool(): ReactElement {
|
||||
style="primary"
|
||||
size="small"
|
||||
onClick={() => setShowAdd(true)}
|
||||
disabled={isInPurgatory || !isAssetNetwork}
|
||||
disabled={isInPurgatory}
|
||||
>
|
||||
Add Liquidity
|
||||
</Button>
|
||||
|
@ -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);
|
||||
}
|
65
src/components/organisms/AssetActions/Trade/PriceImpact.tsx
Normal file
65
src/components/organisms/AssetActions/Trade/PriceImpact.tsx
Normal 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>
|
||||
)
|
||||
}
|
@ -4,13 +4,15 @@
|
||||
margin-left: -2rem;
|
||||
margin-right: -2rem;
|
||||
padding: calc(var(--spacer) / 4) var(--spacer);
|
||||
text-align: center;
|
||||
color: var(--color-secondary);
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: calc(var(--spacer) / 3);
|
||||
}
|
||||
|
||||
.slippage strong {
|
||||
font-weight: var(--font-weight-base);
|
||||
color: var(--font-color-heading);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.title {
|
||||
@ -25,6 +27,4 @@
|
||||
.slippage select {
|
||||
width: fit-content;
|
||||
display: inline-block;
|
||||
margin-left: calc(var(--spacer) / 4);
|
||||
margin-right: calc(var(--spacer) / 4);
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
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 InputElement from '../../../atoms/Input/InputElement'
|
||||
import Tooltip from '../../../atoms/Tooltip'
|
||||
import styles from './Slippage.module.css'
|
||||
|
||||
export default function Slippage(): ReactElement {
|
||||
@ -14,9 +15,9 @@ export default function Slippage(): ReactElement {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.slippage}>
|
||||
<strong>Expected price impact</strong>
|
||||
<div className={styles.slippage}>
|
||||
<strong>Slippage Tolerance</strong>
|
||||
<div>
|
||||
<InputElement
|
||||
name="slippage"
|
||||
type="select"
|
||||
@ -27,7 +28,8 @@ export default function Slippage(): ReactElement {
|
||||
value={values.slippage}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<Tooltip content="Your transaction will revert if the price changes unfavorably by more than this percentage." />
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import Output from './Output'
|
||||
import Slippage from './Slippage'
|
||||
import { FormTradeData, TradeItem } from '../../../../models/FormTrade'
|
||||
import { useOcean } from '../../../../providers/Ocean'
|
||||
import PriceImpact from './PriceImpact'
|
||||
|
||||
export default function Swap({
|
||||
ddo,
|
||||
@ -47,6 +48,11 @@ export default function Swap({
|
||||
validateForm
|
||||
}: 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(() => {
|
||||
if (!ddo || !balance || !values || !price) return
|
||||
|
||||
@ -106,16 +112,63 @@ export default function Swap({
|
||||
}
|
||||
|
||||
const handleValueChange = async (name: string, value: number) => {
|
||||
const newValue =
|
||||
name === 'ocean'
|
||||
? values.type === 'sell'
|
||||
? 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())
|
||||
let tokenIn = ''
|
||||
let tokenOut = ''
|
||||
let newValue
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
@ -139,6 +192,11 @@ export default function Swap({
|
||||
|
||||
<Output dtSymbol={dtItem.token} poolAddress={price?.address} />
|
||||
|
||||
<PriceImpact
|
||||
totalValue={totalValue}
|
||||
tokenAmount={tokenAmount}
|
||||
spotPrice={spotPrice}
|
||||
/>
|
||||
<Slippage />
|
||||
</div>
|
||||
)
|
||||
|
@ -410,6 +410,21 @@ export async function getPrice(asset: DDO): Promise<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(
|
||||
assets: DDO[]
|
||||
): Promise<AssetListPrices[]> {
|
||||
|
Loading…
Reference in New Issue
Block a user