trade form ux improvements
This commit is contained in:
parent
2ef6070991
commit
201a8ad62a
|
@ -370,7 +370,7 @@ const AccountPage = () => {
|
|||
<div className="col-span-4 flex border-t border-th-bkg-3 py-3 pl-6 lg:col-span-1 lg:border-l lg:border-t-0">
|
||||
<div id="account-step-five">
|
||||
<Tooltip
|
||||
content="The value of collateral you have to open new trades or borrows. When your free collateral reaches $0 you won't be able to make withdrawals."
|
||||
content="The amount of capital you have to use for trades and loans. When your free collateral reaches $0 you won't be able to trade, borrow or withdraw."
|
||||
maxWidth="20rem"
|
||||
placement="bottom"
|
||||
delay={250}
|
||||
|
|
|
@ -4,7 +4,7 @@ interface ButtonGroupProps {
|
|||
activeValue: string
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
onChange: (x: string) => void
|
||||
onChange: (x: any) => void
|
||||
unit?: string
|
||||
values: Array<any>
|
||||
names?: Array<string>
|
||||
|
|
|
@ -35,13 +35,25 @@ const TabButtons: FunctionComponent<TabButtonsProps> = ({
|
|||
: ''
|
||||
} ${
|
||||
label === activeValue
|
||||
? 'bg-th-bkg-2 text-th-active'
|
||||
? label === 'buy'
|
||||
? 'bg-th-up-dark font-display text-th-fgd-1'
|
||||
: label === 'sell'
|
||||
? 'bg-th-down-dark font-display text-th-fgd-1'
|
||||
: 'bg-th-bkg-2 text-th-active'
|
||||
: 'hover:cursor-pointer hover:text-th-fgd-2'
|
||||
}`}
|
||||
key={`${label}${i}`}
|
||||
onClick={() => onChange(label)}
|
||||
>
|
||||
<span className="font-medium leading-tight">{t(label)}</span>
|
||||
<span
|
||||
className={`${
|
||||
label === 'buy' || label === 'sell'
|
||||
? 'font-display'
|
||||
: 'font-medium'
|
||||
} leading-tight`}
|
||||
>
|
||||
{t(label)}
|
||||
</span>
|
||||
{count ? (
|
||||
<div
|
||||
className={`ml-1.5 rounded ${
|
||||
|
|
|
@ -13,7 +13,13 @@ const TabUnderline = ({
|
|||
}) => {
|
||||
const { t } = useTranslation('common')
|
||||
return (
|
||||
<div className={`relative mb-3 border-b border-th-bkg-3 pb-1 md:-mt-2.5`}>
|
||||
<div
|
||||
className={`relative mb-3 border-b border-th-bkg-3 ${
|
||||
values.includes('buy') || values.includes('sell')
|
||||
? 'pb-1 font-display md:pb-2.5'
|
||||
: 'pb-1 font-bold'
|
||||
} md:-mt-2.5`}
|
||||
>
|
||||
<div
|
||||
className={`default-transition absolute bottom-[-1px] left-0 h-0.5 ${
|
||||
activeValue === 'buy'
|
||||
|
@ -35,7 +41,7 @@ const TabUnderline = ({
|
|||
<button
|
||||
onClick={() => onChange(value)}
|
||||
className={`default-transition relative flex h-10 w-1/2
|
||||
cursor-pointer items-center justify-center whitespace-nowrap rounded py-1 font-bold md:h-auto md:rounded-none md:hover:opacity-100 ${
|
||||
cursor-pointer items-center justify-center whitespace-nowrap rounded py-1 md:h-auto md:rounded-none md:hover:opacity-100 ${
|
||||
small ? 'text-sm' : 'text-sm lg:text-base'
|
||||
}
|
||||
${
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import {
|
||||
HealthType,
|
||||
PerpMarket,
|
||||
PerpOrderSide,
|
||||
PerpOrderType,
|
||||
|
@ -10,7 +9,6 @@ import {
|
|||
} from '@blockworks-foundation/mango-v4'
|
||||
import Checkbox from '@components/forms/Checkbox'
|
||||
import Button from '@components/shared/Button'
|
||||
import TabButtons from '@components/shared/TabButtons'
|
||||
import Tooltip from '@components/shared/Tooltip'
|
||||
import mangoStore from '@store/mangoStore'
|
||||
import Decimal from 'decimal.js'
|
||||
|
@ -28,7 +26,6 @@ import { QuestionMarkCircleIcon } from '@heroicons/react/20/solid'
|
|||
import Loading from '@components/shared/Loading'
|
||||
import TabUnderline from '@components/shared/TabUnderline'
|
||||
import PerpSlider from './PerpSlider'
|
||||
import HealthImpact from '@components/shared/HealthImpact'
|
||||
import useLocalStorageState from 'hooks/useLocalStorageState'
|
||||
import { SIZE_INPUT_UI_KEY } from 'utils/constants'
|
||||
import SpotButtonGroup from './SpotButtonGroup'
|
||||
|
@ -36,20 +33,20 @@ import PerpButtonGroup from './PerpButtonGroup'
|
|||
import SolBalanceWarnings from '@components/shared/SolBalanceWarnings'
|
||||
import useJupiterMints from 'hooks/useJupiterMints'
|
||||
import useSelectedMarket from 'hooks/useSelectedMarket'
|
||||
import Slippage from './Slippage'
|
||||
import { formatFixedDecimals, getDecimalCount } from 'utils/numbers'
|
||||
import { getDecimalCount, trimDecimals } from 'utils/numbers'
|
||||
import LogoWithFallback from '@components/shared/LogoWithFallback'
|
||||
import useIpAddress from 'hooks/useIpAddress'
|
||||
|
||||
const TABS: [string, number][] = [
|
||||
['Limit', 0],
|
||||
['Market', 0],
|
||||
]
|
||||
import ButtonGroup from '@components/forms/ButtonGroup'
|
||||
import TradeSummary from './TradeSummary'
|
||||
import useMangoAccount from 'hooks/useMangoAccount'
|
||||
import MaxAmountButton from '@components/shared/MaxAmountButton'
|
||||
import { FadeInFadeOut } from '@components/shared/Transitions'
|
||||
|
||||
const set = mangoStore.getState().set
|
||||
|
||||
const AdvancedTradeForm = () => {
|
||||
const { t } = useTranslation(['common', 'trade'])
|
||||
const { mangoAccount } = useMangoAccount()
|
||||
const tradeForm = mangoStore((s) => s.tradeForm)
|
||||
const { mangoTokens } = useJupiterMints()
|
||||
const { selectedMarket, price: oraclePrice } = useSelectedMarket()
|
||||
|
@ -236,6 +233,42 @@ const AdvancedTradeForm = () => {
|
|||
}
|
||||
}, [oraclePrice, selectedMarket, tickDecimals, tradeForm])
|
||||
|
||||
const leverageMax = useMemo(() => {
|
||||
const group = mangoStore.getState().group
|
||||
if (!mangoAccount || !group || !selectedMarket) return 100
|
||||
|
||||
try {
|
||||
if (selectedMarket instanceof Serum3Market) {
|
||||
if (tradeForm.side === 'buy') {
|
||||
return mangoAccount.getMaxQuoteForSerum3BidUi(
|
||||
group,
|
||||
selectedMarket.serumMarketExternal
|
||||
)
|
||||
} else {
|
||||
return mangoAccount.getMaxBaseForSerum3AskUi(
|
||||
group,
|
||||
selectedMarket.serumMarketExternal
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (tradeForm.side === 'buy') {
|
||||
return mangoAccount.getMaxQuoteForPerpBidUi(
|
||||
group,
|
||||
selectedMarket.perpMarketIndex
|
||||
)
|
||||
} else {
|
||||
return mangoAccount.getMaxBaseForPerpAskUi(
|
||||
group,
|
||||
selectedMarket.perpMarketIndex
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error calculating max leverage: spot btn group: ', e)
|
||||
return 0
|
||||
}
|
||||
}, [mangoAccount, tradeForm.side, selectedMarket])
|
||||
|
||||
const handlePlaceOrder = useCallback(async () => {
|
||||
const client = mangoStore.getState().client
|
||||
const group = mangoStore.getState().group
|
||||
|
@ -324,79 +357,99 @@ const AdvancedTradeForm = () => {
|
|||
}
|
||||
}, [])
|
||||
|
||||
const maintProjectedHealth = useMemo(() => {
|
||||
const minOrderDecimals = useMemo(() => {
|
||||
const group = mangoStore.getState().group
|
||||
const mangoAccount = mangoStore.getState().mangoAccount.current
|
||||
|
||||
if (!mangoAccount || !group || !Number.isFinite(Number(tradeForm.baseSize)))
|
||||
return 100
|
||||
|
||||
let simulatedHealthRatio = 0
|
||||
try {
|
||||
if (selectedMarket instanceof Serum3Market) {
|
||||
simulatedHealthRatio =
|
||||
tradeForm.side === 'sell'
|
||||
? mangoAccount.simHealthRatioWithSerum3AskUiChanges(
|
||||
group,
|
||||
Number(tradeForm.baseSize),
|
||||
selectedMarket.serumMarketExternal,
|
||||
HealthType.maint
|
||||
)
|
||||
: mangoAccount.simHealthRatioWithSerum3BidUiChanges(
|
||||
group,
|
||||
Number(tradeForm.quoteSize),
|
||||
selectedMarket.serumMarketExternal,
|
||||
HealthType.maint
|
||||
)
|
||||
} else if (selectedMarket instanceof PerpMarket) {
|
||||
simulatedHealthRatio =
|
||||
tradeForm.side === 'sell'
|
||||
? mangoAccount.simHealthRatioWithPerpAskUiChanges(
|
||||
group,
|
||||
selectedMarket.perpMarketIndex,
|
||||
parseFloat(tradeForm.baseSize)
|
||||
)
|
||||
: mangoAccount.simHealthRatioWithPerpBidUiChanges(
|
||||
group,
|
||||
selectedMarket.perpMarketIndex,
|
||||
parseFloat(tradeForm.baseSize)
|
||||
)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error calculating projected health: ', e)
|
||||
if (!group || !selectedMarket) return 1
|
||||
let minOrderDecimals = 1
|
||||
if (selectedMarket instanceof Serum3Market) {
|
||||
const market = group.getSerum3ExternalMarket(
|
||||
selectedMarket.serumMarketExternal
|
||||
)
|
||||
minOrderDecimals = getDecimalCount(market.minOrderSize)
|
||||
} else {
|
||||
minOrderDecimals = getDecimalCount(selectedMarket.minOrderSize)
|
||||
}
|
||||
return minOrderDecimals
|
||||
}, [selectedMarket])
|
||||
|
||||
return simulatedHealthRatio > 100
|
||||
? 100
|
||||
: simulatedHealthRatio < 0
|
||||
? 0
|
||||
: Math.trunc(simulatedHealthRatio)
|
||||
}, [selectedMarket, tradeForm.baseSize, tradeForm.side])
|
||||
const handleMax = useCallback(() => {
|
||||
const set = mangoStore.getState().set
|
||||
set((state) => {
|
||||
if (tradeForm.side === 'buy') {
|
||||
state.tradeForm.quoteSize = trimDecimals(
|
||||
leverageMax,
|
||||
tickDecimals
|
||||
).toFixed(tickDecimals)
|
||||
if (tradeForm.tradeType === 'Market' || !tradeForm.price) {
|
||||
state.tradeForm.baseSize = trimDecimals(
|
||||
leverageMax / oraclePrice,
|
||||
minOrderDecimals
|
||||
).toFixed(minOrderDecimals)
|
||||
} else {
|
||||
state.tradeForm.baseSize = trimDecimals(
|
||||
leverageMax / parseFloat(tradeForm.price),
|
||||
minOrderDecimals
|
||||
).toFixed(minOrderDecimals)
|
||||
}
|
||||
} else {
|
||||
state.tradeForm.baseSize = trimDecimals(
|
||||
leverageMax,
|
||||
tickDecimals
|
||||
).toFixed(tickDecimals)
|
||||
if (tradeForm.tradeType === 'Market' || !tradeForm.price) {
|
||||
state.tradeForm.quoteSize = trimDecimals(
|
||||
leverageMax * oraclePrice,
|
||||
minOrderDecimals
|
||||
).toFixed(minOrderDecimals)
|
||||
} else {
|
||||
state.tradeForm.quoteSize = trimDecimals(
|
||||
leverageMax * parseFloat(tradeForm.price),
|
||||
minOrderDecimals
|
||||
).toFixed(minOrderDecimals)
|
||||
}
|
||||
}
|
||||
})
|
||||
}, [leverageMax, tradeForm])
|
||||
|
||||
const maxAmount = useMemo(() => {
|
||||
if (!tradeForm.price) return '0'
|
||||
if (tradeForm.side === 'buy') {
|
||||
return trimDecimals(
|
||||
leverageMax / parseFloat(tradeForm.price),
|
||||
tickDecimals
|
||||
).toFixed(tickDecimals)
|
||||
} else {
|
||||
return trimDecimals(leverageMax, minOrderDecimals).toFixed(
|
||||
minOrderDecimals
|
||||
)
|
||||
}
|
||||
}, [leverageMax, minOrderDecimals, tickDecimals, tradeForm])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="border-b border-th-bkg-3 md:border-t lg:border-t-0">
|
||||
<TabButtons
|
||||
activeValue={tradeForm.tradeType}
|
||||
onChange={(tab: 'Limit' | 'Market') => setTradeType(tab)}
|
||||
values={TABS}
|
||||
fillWidth
|
||||
<div className="mt-1.5 px-2 md:mt-0 md:border-t md:border-th-bkg-3 md:px-4 md:pt-5 lg:mt-5 lg:border-t-0 lg:pt-0">
|
||||
<TabUnderline
|
||||
activeValue={tradeForm.side}
|
||||
values={['buy', 'sell']}
|
||||
onChange={(v) => handleSetSide(v)}
|
||||
small
|
||||
/>
|
||||
</div>
|
||||
<div className="px-3 md:px-4">
|
||||
<SolBalanceWarnings />
|
||||
</div>
|
||||
<div className="mt-1 px-2 md:mt-6 md:px-4">
|
||||
<TabUnderline
|
||||
activeValue={tradeForm.side}
|
||||
values={['buy', 'sell']}
|
||||
onChange={(v) => handleSetSide(v)}
|
||||
<div className="mt-1 px-2 md:mt-3 md:px-4">
|
||||
<p className="mb-2 text-xs">{t('trade:order-type')}</p>
|
||||
<ButtonGroup
|
||||
activeValue={tradeForm.tradeType}
|
||||
onChange={(tab: 'Limit' | 'Market') => setTradeType(tab)}
|
||||
values={['Limit', 'Market']}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 px-3 md:px-4">
|
||||
<div className="mt-3 px-3 md:px-4">
|
||||
{tradeForm.tradeType === 'Limit' ? (
|
||||
<>
|
||||
<div className="mb-2 mt-4 flex items-center justify-between">
|
||||
<div className="mb-2 mt-3 flex items-center justify-between">
|
||||
<p className="text-xs text-th-fgd-3">{t('trade:limit-price')}</p>
|
||||
</div>
|
||||
<div className="default-transition flex items-center rounded-md border border-th-input-border bg-th-input-bkg p-2 text-sm font-bold text-th-fgd-1 md:hover:border-th-input-border-hover lg:text-base">
|
||||
|
@ -429,7 +482,15 @@ const AdvancedTradeForm = () => {
|
|||
</>
|
||||
) : null}
|
||||
<div className="mb-2 mt-3 flex items-center justify-between">
|
||||
<p className="text-xs text-th-fgd-3">{t('trade:amount')}</p>
|
||||
<p className="text-xs text-th-fgd-3">{t('trade:size')}</p>
|
||||
<FadeInFadeOut show={!!tradeForm.price}>
|
||||
<MaxAmountButton
|
||||
className="text-xs"
|
||||
label={t('max')}
|
||||
onClick={handleMax}
|
||||
value={maxAmount}
|
||||
/>
|
||||
</FadeInFadeOut>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="default-transition flex items-center rounded-md rounded-b-none border border-th-input-border bg-th-input-bkg p-2 text-sm font-bold text-th-fgd-1 md:hover:z-10 md:hover:border-th-input-border-hover lg:text-base">
|
||||
|
@ -494,20 +555,36 @@ const AdvancedTradeForm = () => {
|
|||
<div className="mt-2 flex">
|
||||
{selectedMarket instanceof Serum3Market ? (
|
||||
tradeFormSizeUi === 'slider' ? (
|
||||
<SpotSlider />
|
||||
<SpotSlider
|
||||
max={leverageMax}
|
||||
minOrderDecimals={minOrderDecimals}
|
||||
tickDecimals={tickDecimals}
|
||||
/>
|
||||
) : (
|
||||
<SpotButtonGroup />
|
||||
<SpotButtonGroup
|
||||
max={leverageMax}
|
||||
minOrderDecimals={minOrderDecimals}
|
||||
tickDecimals={tickDecimals}
|
||||
/>
|
||||
)
|
||||
) : tradeFormSizeUi === 'slider' ? (
|
||||
<PerpSlider />
|
||||
<PerpSlider
|
||||
max={leverageMax}
|
||||
minOrderDecimals={minOrderDecimals}
|
||||
tickDecimals={tickDecimals}
|
||||
/>
|
||||
) : (
|
||||
<PerpButtonGroup />
|
||||
<PerpButtonGroup
|
||||
max={leverageMax}
|
||||
minOrderDecimals={minOrderDecimals}
|
||||
tickDecimals={tickDecimals}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap px-5">
|
||||
<div className="flex flex-wrap px-5 md:flex-nowrap">
|
||||
{tradeForm.tradeType === 'Limit' ? (
|
||||
<div className="flex">
|
||||
<div className="mr-4 mt-4" id="trade-step-six">
|
||||
<div className="mr-3 mt-4" id="trade-step-six">
|
||||
<Tooltip
|
||||
className="hidden md:block"
|
||||
delay={250}
|
||||
|
@ -522,7 +599,7 @@ const AdvancedTradeForm = () => {
|
|||
</Checkbox>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="mr-4 mt-4" id="trade-step-seven">
|
||||
<div className="mr-3 mt-4" id="trade-step-seven">
|
||||
<Tooltip
|
||||
className="hidden md:block"
|
||||
delay={250}
|
||||
|
@ -558,7 +635,7 @@ const AdvancedTradeForm = () => {
|
|||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-6 flex px-3 md:px-4">
|
||||
<div className="mt-6 mb-4 flex px-3 md:px-4">
|
||||
{ipAllowed ? (
|
||||
<Button
|
||||
onClick={handlePlaceOrder}
|
||||
|
@ -595,21 +672,7 @@ const AdvancedTradeForm = () => {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-4 space-y-2 px-3 md:px-4 lg:mt-6">
|
||||
{tradeForm.price && tradeForm.baseSize ? (
|
||||
<div className="flex justify-between text-xs">
|
||||
<p>{t('trade:order-value')}</p>
|
||||
<p className="text-th-fgd-2">
|
||||
{formatFixedDecimals(
|
||||
parseFloat(tradeForm.price) * parseFloat(tradeForm.baseSize),
|
||||
true
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
<HealthImpact maintProjectedHealth={maintProjectedHealth} small />
|
||||
<Slippage />
|
||||
</div>
|
||||
<TradeSummary mangoAccount={mangoAccount} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,72 +1,54 @@
|
|||
import { PerpMarket } from '@blockworks-foundation/mango-v4'
|
||||
import ButtonGroup from '@components/forms/ButtonGroup'
|
||||
import mangoStore from '@store/mangoStore'
|
||||
import useMangoAccount from 'hooks/useMangoAccount'
|
||||
import useSelectedMarket from 'hooks/useSelectedMarket'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
// import { notify } from 'utils/notifications'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { trimDecimals } from 'utils/numbers'
|
||||
|
||||
const PerpButtonGroup = () => {
|
||||
const side = mangoStore((s) => s.tradeForm.side)
|
||||
const { selectedMarket } = useSelectedMarket()
|
||||
const { mangoAccount } = useMangoAccount()
|
||||
const PerpButtonGroup = ({
|
||||
max,
|
||||
minOrderDecimals,
|
||||
tickDecimals,
|
||||
}: {
|
||||
max: number
|
||||
minOrderDecimals: number
|
||||
tickDecimals: number
|
||||
}) => {
|
||||
const [sizePercentage, setSizePercentage] = useState('')
|
||||
const tradeFormPrice = mangoStore((s) => s.tradeForm.price)
|
||||
|
||||
const leverageMax = useMemo(() => {
|
||||
const group = mangoStore.getState().group
|
||||
if (!mangoAccount || !group || !selectedMarket) return 100
|
||||
if (!(selectedMarket instanceof PerpMarket)) return 100
|
||||
|
||||
try {
|
||||
if (side === 'buy') {
|
||||
return mangoAccount.getMaxQuoteForPerpBidUi(
|
||||
group,
|
||||
selectedMarket.perpMarketIndex
|
||||
)
|
||||
} else {
|
||||
return mangoAccount.getMaxBaseForPerpAskUi(
|
||||
group,
|
||||
selectedMarket.perpMarketIndex
|
||||
)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error calculating max leverage perp btn grp: ', e)
|
||||
// notify({
|
||||
// type: 'error',
|
||||
// title: 'Error calculating max leverage.',
|
||||
// })
|
||||
return 0
|
||||
}
|
||||
}, [side, selectedMarket, mangoAccount, tradeFormPrice])
|
||||
|
||||
const handleSizePercentage = useCallback(
|
||||
(percentage: string) => {
|
||||
const set = mangoStore.getState().set
|
||||
setSizePercentage(percentage)
|
||||
const size = leverageMax * (Number(percentage) / 100)
|
||||
const size = max * (Number(percentage) / 100)
|
||||
|
||||
set((s) => {
|
||||
if (s.tradeForm.side === 'buy') {
|
||||
s.tradeForm.quoteSize = size.toString()
|
||||
s.tradeForm.quoteSize = trimDecimals(size, tickDecimals).toFixed(
|
||||
tickDecimals
|
||||
)
|
||||
|
||||
if (Number(s.tradeForm.price)) {
|
||||
s.tradeForm.baseSize = (size / Number(s.tradeForm.price)).toString()
|
||||
s.tradeForm.baseSize = trimDecimals(
|
||||
size / Number(s.tradeForm.price),
|
||||
minOrderDecimals
|
||||
).toFixed(minOrderDecimals)
|
||||
} else {
|
||||
s.tradeForm.baseSize = ''
|
||||
}
|
||||
} else if (s.tradeForm.side === 'sell') {
|
||||
s.tradeForm.baseSize = size.toString()
|
||||
s.tradeForm.baseSize = trimDecimals(size, minOrderDecimals).toFixed(
|
||||
minOrderDecimals
|
||||
)
|
||||
|
||||
if (Number(s.tradeForm.price)) {
|
||||
s.tradeForm.quoteSize = (
|
||||
size * Number(s.tradeForm.price)
|
||||
).toString()
|
||||
s.tradeForm.quoteSize = trimDecimals(
|
||||
size * Number(s.tradeForm.price),
|
||||
tickDecimals
|
||||
).toFixed(tickDecimals)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
[leverageMax]
|
||||
[max, minOrderDecimals, tickDecimals]
|
||||
)
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
import { PerpMarket } from '@blockworks-foundation/mango-v4'
|
||||
import LeverageSlider from '@components/shared/LeverageSlider'
|
||||
import mangoStore from '@store/mangoStore'
|
||||
import useMangoAccount from 'hooks/useMangoAccount'
|
||||
import useSelectedMarket from 'hooks/useSelectedMarket'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
// import { notify } from 'utils/notifications'
|
||||
import { trimDecimals } from 'utils/numbers'
|
||||
|
||||
const PerpSlider = () => {
|
||||
const side = mangoStore((s) => s.tradeForm.side)
|
||||
const PerpSlider = ({
|
||||
max,
|
||||
minOrderDecimals,
|
||||
tickDecimals,
|
||||
}: {
|
||||
max: number
|
||||
minOrderDecimals: number
|
||||
tickDecimals: number
|
||||
}) => {
|
||||
const { selectedMarket, price: marketPrice } = useSelectedMarket()
|
||||
const { mangoAccount } = useMangoAccount()
|
||||
const tradeForm = mangoStore((s) => s.tradeForm)
|
||||
|
||||
const step = useMemo(() => {
|
||||
|
@ -19,33 +24,6 @@ const PerpSlider = () => {
|
|||
return 0.01
|
||||
}, [selectedMarket])
|
||||
|
||||
const leverageMax = useMemo(() => {
|
||||
const group = mangoStore.getState().group
|
||||
if (!mangoAccount || !group || !selectedMarket) return 100
|
||||
if (!(selectedMarket instanceof PerpMarket)) return 100
|
||||
|
||||
try {
|
||||
if (side === 'buy') {
|
||||
return mangoAccount.getMaxQuoteForPerpBidUi(
|
||||
group,
|
||||
selectedMarket.perpMarketIndex
|
||||
)
|
||||
} else {
|
||||
return mangoAccount.getMaxBaseForPerpAskUi(
|
||||
group,
|
||||
selectedMarket.perpMarketIndex
|
||||
)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error calculating max leverage for PerpSlider: ', e)
|
||||
// notify({
|
||||
// type: 'error',
|
||||
// title: 'Error calculating max leverage.',
|
||||
// })
|
||||
return 0
|
||||
}
|
||||
}, [side, selectedMarket, mangoAccount])
|
||||
|
||||
const handleSlide = useCallback(
|
||||
(val: string) => {
|
||||
const set = mangoStore.getState().set
|
||||
|
@ -59,19 +37,25 @@ const PerpSlider = () => {
|
|||
if (s.tradeForm.side === 'buy') {
|
||||
s.tradeForm.quoteSize = val
|
||||
if (Number(price)) {
|
||||
s.tradeForm.baseSize = (parseFloat(val) / price).toString()
|
||||
s.tradeForm.baseSize = trimDecimals(
|
||||
parseFloat(val) / price,
|
||||
minOrderDecimals
|
||||
).toFixed(minOrderDecimals)
|
||||
} else {
|
||||
s.tradeForm.baseSize = ''
|
||||
}
|
||||
} else if (s.tradeForm.side === 'sell') {
|
||||
s.tradeForm.baseSize = val
|
||||
if (Number(price)) {
|
||||
s.tradeForm.quoteSize = (parseFloat(val) * price).toString()
|
||||
s.tradeForm.quoteSize = trimDecimals(
|
||||
parseFloat(val) * price,
|
||||
tickDecimals
|
||||
).toFixed(tickDecimals)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
[marketPrice]
|
||||
[marketPrice, minOrderDecimals, tickDecimals]
|
||||
)
|
||||
|
||||
return (
|
||||
|
@ -82,7 +66,7 @@ const PerpSlider = () => {
|
|||
? parseFloat(tradeForm.quoteSize)
|
||||
: parseFloat(tradeForm.baseSize)
|
||||
}
|
||||
leverageMax={leverageMax}
|
||||
leverageMax={max}
|
||||
onChange={handleSlide}
|
||||
step={step}
|
||||
/>
|
||||
|
|
|
@ -1,71 +1,59 @@
|
|||
import { Serum3Market } from '@blockworks-foundation/mango-v4'
|
||||
import ButtonGroup from '@components/forms/ButtonGroup'
|
||||
import mangoStore from '@store/mangoStore'
|
||||
import useMangoAccount from 'hooks/useMangoAccount'
|
||||
import useSelectedMarket from 'hooks/useSelectedMarket'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
// import { notify } from 'utils/notifications'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { trimDecimals } from 'utils/numbers'
|
||||
|
||||
const SpotButtonGroup = () => {
|
||||
const SpotButtonGroup = ({
|
||||
max,
|
||||
minOrderDecimals,
|
||||
tickDecimals,
|
||||
}: {
|
||||
max: number
|
||||
minOrderDecimals: number
|
||||
tickDecimals: number
|
||||
}) => {
|
||||
const side = mangoStore((s) => s.tradeForm.side)
|
||||
const { selectedMarket } = useSelectedMarket()
|
||||
const { mangoAccount } = useMangoAccount()
|
||||
const [sizePercentage, setSizePercentage] = useState('')
|
||||
|
||||
const leverageMax = useMemo(() => {
|
||||
const group = mangoStore.getState().group
|
||||
if (!mangoAccount || !group || !selectedMarket) return 100
|
||||
if (!(selectedMarket instanceof Serum3Market)) return 100
|
||||
|
||||
try {
|
||||
if (side === 'buy') {
|
||||
return mangoAccount.getMaxQuoteForSerum3BidUi(
|
||||
group,
|
||||
selectedMarket.serumMarketExternal
|
||||
)
|
||||
} else {
|
||||
return mangoAccount.getMaxBaseForSerum3AskUi(
|
||||
group,
|
||||
selectedMarket.serumMarketExternal
|
||||
)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error calculating max leverage: spot btn group: ', e)
|
||||
// notify({
|
||||
// type: 'error',
|
||||
// title: 'Error calculating max leverage.',
|
||||
// })
|
||||
return 0
|
||||
}
|
||||
}, [side, selectedMarket, mangoAccount])
|
||||
|
||||
const handleSizePercentage = useCallback(
|
||||
(percentage: string) => {
|
||||
const set = mangoStore.getState().set
|
||||
setSizePercentage(percentage)
|
||||
const size = leverageMax * (Number(percentage) / 100)
|
||||
const size = max * (Number(percentage) / 100)
|
||||
|
||||
set((s) => {
|
||||
if (s.tradeForm.side === 'buy') {
|
||||
s.tradeForm.quoteSize = size.toString()
|
||||
s.tradeForm.quoteSize = trimDecimals(size, tickDecimals).toFixed(
|
||||
tickDecimals
|
||||
)
|
||||
|
||||
if (Number(s.tradeForm.price)) {
|
||||
s.tradeForm.baseSize = (size / Number(s.tradeForm.price)).toString()
|
||||
s.tradeForm.baseSize = trimDecimals(
|
||||
size / Number(s.tradeForm.price),
|
||||
minOrderDecimals
|
||||
).toFixed(minOrderDecimals)
|
||||
} else {
|
||||
s.tradeForm.baseSize = ''
|
||||
}
|
||||
} else if (s.tradeForm.side === 'sell') {
|
||||
s.tradeForm.baseSize = size.toString()
|
||||
s.tradeForm.baseSize = trimDecimals(size, tickDecimals).toFixed(
|
||||
minOrderDecimals
|
||||
)
|
||||
|
||||
if (Number(s.tradeForm.price)) {
|
||||
s.tradeForm.quoteSize = (
|
||||
size * Number(s.tradeForm.price)
|
||||
).toString()
|
||||
s.tradeForm.quoteSize = trimDecimals(
|
||||
size * Number(s.tradeForm.price),
|
||||
tickDecimals
|
||||
).toFixed(tickDecimals)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
[side, selectedMarket, mangoAccount]
|
||||
[side, selectedMarket, mangoAccount, minOrderDecimals, tickDecimals]
|
||||
)
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,44 +1,21 @@
|
|||
import { Serum3Market } from '@blockworks-foundation/mango-v4'
|
||||
import LeverageSlider from '@components/shared/LeverageSlider'
|
||||
import mangoStore from '@store/mangoStore'
|
||||
import useMangoAccount from 'hooks/useMangoAccount'
|
||||
import useSelectedMarket from 'hooks/useSelectedMarket'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
// import { notify } from 'utils/notifications'
|
||||
import { useCallback } from 'react'
|
||||
import { trimDecimals } from 'utils/numbers'
|
||||
|
||||
const SpotSlider = () => {
|
||||
const side = mangoStore((s) => s.tradeForm.side)
|
||||
const { selectedMarket, price: marketPrice } = useSelectedMarket()
|
||||
const { mangoAccount } = useMangoAccount()
|
||||
const SpotSlider = ({
|
||||
max,
|
||||
minOrderDecimals,
|
||||
tickDecimals,
|
||||
}: {
|
||||
max: number
|
||||
minOrderDecimals: number
|
||||
tickDecimals: number
|
||||
}) => {
|
||||
const { price: marketPrice } = useSelectedMarket()
|
||||
const tradeForm = mangoStore((s) => s.tradeForm)
|
||||
|
||||
const leverageMax = useMemo(() => {
|
||||
const group = mangoStore.getState().group
|
||||
if (!mangoAccount || !group || !selectedMarket) return 100
|
||||
if (!(selectedMarket instanceof Serum3Market)) return 100
|
||||
|
||||
try {
|
||||
if (side === 'buy') {
|
||||
return mangoAccount.getMaxQuoteForSerum3BidUi(
|
||||
group,
|
||||
selectedMarket.serumMarketExternal
|
||||
)
|
||||
} else {
|
||||
return mangoAccount.getMaxBaseForSerum3AskUi(
|
||||
group,
|
||||
selectedMarket.serumMarketExternal
|
||||
)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error calculating max leverage for spot slider: ', e)
|
||||
// notify({
|
||||
// type: 'error',
|
||||
// title: 'Error calculating max leverage.',
|
||||
// })
|
||||
return 0
|
||||
}
|
||||
}, [side, selectedMarket, mangoAccount])
|
||||
|
||||
const handleSlide = useCallback(
|
||||
(val: string) => {
|
||||
const set = mangoStore.getState().set
|
||||
|
@ -52,19 +29,25 @@ const SpotSlider = () => {
|
|||
if (s.tradeForm.side === 'buy') {
|
||||
s.tradeForm.quoteSize = val
|
||||
if (Number(price)) {
|
||||
s.tradeForm.baseSize = (parseFloat(val) / price).toString()
|
||||
s.tradeForm.baseSize = trimDecimals(
|
||||
parseFloat(val) / price,
|
||||
minOrderDecimals
|
||||
).toFixed(minOrderDecimals)
|
||||
} else {
|
||||
s.tradeForm.baseSize = ''
|
||||
}
|
||||
} else if (s.tradeForm.side === 'sell') {
|
||||
s.tradeForm.baseSize = val
|
||||
if (Number(price)) {
|
||||
s.tradeForm.quoteSize = (parseFloat(val) * price).toString()
|
||||
s.tradeForm.quoteSize = trimDecimals(
|
||||
parseFloat(val) * price,
|
||||
tickDecimals
|
||||
).toFixed(tickDecimals)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
[marketPrice]
|
||||
[marketPrice, minOrderDecimals, tickDecimals]
|
||||
)
|
||||
|
||||
return (
|
||||
|
@ -75,7 +58,7 @@ const SpotSlider = () => {
|
|||
? parseFloat(tradeForm.quoteSize)
|
||||
: parseFloat(tradeForm.baseSize)
|
||||
}
|
||||
leverageMax={leverageMax}
|
||||
leverageMax={max}
|
||||
onChange={handleSlide}
|
||||
step={0.01}
|
||||
/>
|
||||
|
|
|
@ -140,7 +140,7 @@ const TradeAdvancedPage = () => {
|
|||
{ i: 'tv-chart', x: 0, y: 1, w: 17, h: 464 },
|
||||
{ i: 'balances', x: 0, y: 2, w: 17, h: 428 + marketHeaderHeight },
|
||||
{ i: 'orderbook', x: 18, y: 2, w: 7, h: 428 + marketHeaderHeight },
|
||||
{ i: 'trade-form', x: 18, y: 1, w: 7, h: 464 + marketHeaderHeight },
|
||||
{ i: 'trade-form', x: 18, y: 1, w: 7, h: 492 + marketHeaderHeight },
|
||||
],
|
||||
}
|
||||
}, [height])
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
import {
|
||||
HealthType,
|
||||
MangoAccount,
|
||||
PerpMarket,
|
||||
Serum3Market,
|
||||
toUiDecimalsForQuote,
|
||||
} from '@blockworks-foundation/mango-v4'
|
||||
import HealthImpact from '@components/shared/HealthImpact'
|
||||
import Tooltip from '@components/shared/Tooltip'
|
||||
import mangoStore from '@store/mangoStore'
|
||||
import useMangoGroup from 'hooks/useMangoGroup'
|
||||
import useSelectedMarket from 'hooks/useSelectedMarket'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { useMemo } from 'react'
|
||||
import { formatFixedDecimals } from 'utils/numbers'
|
||||
import Slippage from './Slippage'
|
||||
|
||||
const TradeSummary = ({
|
||||
mangoAccount,
|
||||
}: {
|
||||
mangoAccount: MangoAccount | undefined
|
||||
}) => {
|
||||
const { t } = useTranslation(['common', 'trade'])
|
||||
const { group } = useMangoGroup()
|
||||
const tradeForm = mangoStore((s) => s.tradeForm)
|
||||
const { selectedMarket } = useSelectedMarket()
|
||||
|
||||
const maintProjectedHealth = useMemo(() => {
|
||||
if (!mangoAccount || !group || !Number.isFinite(Number(tradeForm.baseSize)))
|
||||
return 100
|
||||
|
||||
let simulatedHealthRatio = 0
|
||||
try {
|
||||
if (selectedMarket instanceof Serum3Market) {
|
||||
simulatedHealthRatio =
|
||||
tradeForm.side === 'sell'
|
||||
? mangoAccount.simHealthRatioWithSerum3AskUiChanges(
|
||||
group,
|
||||
Number(tradeForm.baseSize),
|
||||
selectedMarket.serumMarketExternal,
|
||||
HealthType.maint
|
||||
)
|
||||
: mangoAccount.simHealthRatioWithSerum3BidUiChanges(
|
||||
group,
|
||||
Number(tradeForm.quoteSize),
|
||||
selectedMarket.serumMarketExternal,
|
||||
HealthType.maint
|
||||
)
|
||||
} else if (selectedMarket instanceof PerpMarket) {
|
||||
simulatedHealthRatio =
|
||||
tradeForm.side === 'sell'
|
||||
? mangoAccount.simHealthRatioWithPerpAskUiChanges(
|
||||
group,
|
||||
selectedMarket.perpMarketIndex,
|
||||
parseFloat(tradeForm.baseSize)
|
||||
)
|
||||
: mangoAccount.simHealthRatioWithPerpBidUiChanges(
|
||||
group,
|
||||
selectedMarket.perpMarketIndex,
|
||||
parseFloat(tradeForm.baseSize)
|
||||
)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Error calculating projected health: ', e)
|
||||
}
|
||||
|
||||
return simulatedHealthRatio > 100
|
||||
? 100
|
||||
: simulatedHealthRatio < 0
|
||||
? 0
|
||||
: Math.trunc(simulatedHealthRatio)
|
||||
}, [group, mangoAccount, selectedMarket, tradeForm.baseSize, tradeForm.side])
|
||||
|
||||
return (
|
||||
<div className="space-y-2 px-3 md:px-4">
|
||||
{tradeForm.price && tradeForm.baseSize ? (
|
||||
<div className="flex justify-between text-xs">
|
||||
<p>{t('trade:order-value')}</p>
|
||||
<p className="text-th-fgd-2">
|
||||
{formatFixedDecimals(
|
||||
parseFloat(tradeForm.price) * parseFloat(tradeForm.baseSize),
|
||||
true
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
<HealthImpact maintProjectedHealth={maintProjectedHealth} small />
|
||||
<div className="flex justify-between text-xs">
|
||||
<Tooltip content="The amount of capital you have to use for trades and loans. When your free collateral reaches $0 you won't be able to trade, borrow or withdraw.">
|
||||
<p className="tooltip-underline">{t('free-collateral')}</p>
|
||||
</Tooltip>
|
||||
<p className="text-th-fgd-2">
|
||||
{group && mangoAccount
|
||||
? formatFixedDecimals(
|
||||
toUiDecimalsForQuote(
|
||||
mangoAccount.getCollateralValue(group)!.toNumber()
|
||||
),
|
||||
true
|
||||
)
|
||||
: '–'}
|
||||
</p>
|
||||
</div>
|
||||
<Slippage />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TradeSummary
|
|
@ -21,9 +21,10 @@
|
|||
"no-unsettled": "No unsettled funds",
|
||||
"open-interest": "Open Interest",
|
||||
"oracle-price": "Oracle Price",
|
||||
"orders": "Orders",
|
||||
"order-error": "Failed to place order",
|
||||
"order-type": "Order Type",
|
||||
"order-value": "Order Value",
|
||||
"orders": "Orders",
|
||||
"post": "Post",
|
||||
"place-order": "Place {{side}} Order",
|
||||
"placing-order": "Placing Order",
|
||||
|
|
|
@ -21,9 +21,10 @@
|
|||
"no-unsettled": "No unsettled funds",
|
||||
"open-interest": "Open Interest",
|
||||
"oracle-price": "Oracle Price",
|
||||
"orders": "Orders",
|
||||
"order-error": "Failed to place order",
|
||||
"order-type": "Order Type",
|
||||
"order-value": "Order Value",
|
||||
"orders": "Orders",
|
||||
"post": "Post",
|
||||
"place-order": "Place {{side}} Order",
|
||||
"placing-order": "Placing Order",
|
||||
|
|
|
@ -21,9 +21,10 @@
|
|||
"no-unsettled": "No unsettled funds",
|
||||
"open-interest": "Open Interest",
|
||||
"oracle-price": "Oracle Price",
|
||||
"orders": "Orders",
|
||||
"order-error": "Failed to place order",
|
||||
"order-type": "Order Type",
|
||||
"order-value": "Order Value",
|
||||
"orders": "Orders",
|
||||
"post": "Post",
|
||||
"place-order": "Place {{side}} Order",
|
||||
"placing-order": "Placing Order",
|
||||
|
|
|
@ -21,9 +21,10 @@
|
|||
"no-unsettled": "No unsettled funds",
|
||||
"open-interest": "Open Interest",
|
||||
"oracle-price": "Oracle Price",
|
||||
"orders": "Orders",
|
||||
"order-error": "Failed to place order",
|
||||
"order-type": "Order Type",
|
||||
"order-value": "Order Value",
|
||||
"orders": "Orders",
|
||||
"post": "Post",
|
||||
"place-order": "Place {{side}} Order",
|
||||
"placing-order": "Placing Order",
|
||||
|
|
|
@ -21,9 +21,10 @@
|
|||
"no-unsettled": "No unsettled funds",
|
||||
"open-interest": "Open Interest",
|
||||
"oracle-price": "Oracle Price",
|
||||
"orders": "Orders",
|
||||
"order-error": "Failed to place order",
|
||||
"order-type": "Order Type",
|
||||
"order-value": "Order Value",
|
||||
"orders": "Orders",
|
||||
"post": "Post",
|
||||
"place-order": "Place {{side}} Order",
|
||||
"placing-order": "Placing Order",
|
||||
|
|
Loading…
Reference in New Issue