mango-ui-v3/components/trade_form/AdvancedTradeForm.tsx

1127 lines
34 KiB
TypeScript
Raw Normal View History

import { useEffect, useMemo, useRef, useState } from 'react'
2021-09-26 06:20:51 -07:00
import useIpAddress from '../../hooks/useIpAddress'
2021-09-24 05:24:17 -07:00
import {
2021-12-03 09:31:22 -08:00
clamp,
2021-09-25 05:41:04 -07:00
getMarketIndexBySymbol,
2021-09-24 05:24:17 -07:00
getTokenBySymbol,
2021-09-25 05:41:04 -07:00
I80F48,
nativeI80F48ToUi,
2021-09-24 05:24:17 -07:00
PerpMarket,
PerpOrderType,
2021-09-24 05:24:17 -07:00
} from '@blockworks-foundation/mango-client'
import {
ExclamationIcon,
InformationCircleIcon,
} from '@heroicons/react/outline'
2021-09-26 06:20:51 -07:00
import { notify } from '../../utils/notifications'
import { calculateTradePrice, getDecimalCount } from '../../utils'
import { floorToDecimal } from '../../utils/index'
2021-10-01 08:32:15 -07:00
import useMangoStore, { Orderbook } from '../../stores/useMangoStore'
2021-12-03 02:34:10 -08:00
import Button, { LinkButton } from '../Button'
2021-09-24 05:24:17 -07:00
import TradeType from './TradeType'
2021-09-26 06:20:51 -07:00
import Input from '../Input'
2021-09-24 05:24:17 -07:00
import { Market } from '@project-serum/serum'
import Big from 'big.js'
2021-09-26 06:20:51 -07:00
import Tooltip from '../Tooltip'
2021-09-25 02:40:35 -07:00
import OrderSideTabs from './OrderSideTabs'
2021-09-26 06:20:51 -07:00
import { ElementTitle } from '../styles'
import ButtonGroup from '../ButtonGroup'
import Checkbox from '../Checkbox'
import { useViewport } from '../../hooks/useViewport'
import { breakpoints } from '../TradePageGrid'
2021-09-30 07:21:45 -07:00
import EstPriceImpact from './EstPriceImpact'
2021-10-01 09:21:09 -07:00
import useFees from '../../hooks/useFees'
import { useTranslation } from 'next-i18next'
2021-10-18 13:38:03 -07:00
import useSrmAccount from '../../hooks/useSrmAccount'
2022-01-14 11:44:10 -08:00
import useLocalStorageState, {
useLocalStorageStringState,
} from '../../hooks/useLocalStorageState'
2021-12-03 02:34:10 -08:00
import InlineNotification from '../InlineNotification'
2022-01-14 11:44:10 -08:00
import { DEFAULT_SPOT_MARGIN_KEY } from '../SettingsModal'
2021-12-03 02:34:10 -08:00
const MAX_SLIPPAGE_KEY = 'maxSlippage'
2021-09-24 05:24:17 -07:00
export const TRIGGER_ORDER_TYPES = [
'Stop Loss',
'Take Profit',
'Stop Limit',
'Take Profit Limit',
]
2021-09-26 06:20:51 -07:00
interface AdvancedTradeFormProps {
initLeverage?: number
}
export default function AdvancedTradeForm({
initLeverage,
}: AdvancedTradeFormProps) {
const { t } = useTranslation('common')
2021-09-24 05:24:17 -07:00
const set = useMangoStore((s) => s.set)
2021-11-22 15:54:51 -08:00
const { ipAllowed, spotAllowed } = useIpAddress()
2021-09-24 05:24:17 -07:00
const connected = useMangoStore((s) => s.wallet.connected)
const actions = useMangoStore((s) => s.actions)
const groupConfig = useMangoStore((s) => s.selectedMangoGroup.config)
const marketConfig = useMangoStore((s) => s.selectedMarket.config)
const walletTokens = useMangoStore((s) => s.wallet.tokens)
2021-09-24 05:24:17 -07:00
const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current)
const mangoClient = useMangoStore((s) => s.connection.client)
const market = useMangoStore((s) => s.selectedMarket.current)
const isPerpMarket = market instanceof PerpMarket
const [reduceOnly, setReduceOnly] = useState(false)
2022-01-14 11:44:10 -08:00
const [defaultSpotMargin] = useLocalStorageState(
DEFAULT_SPOT_MARGIN_KEY,
false
)
const [spotMargin, setSpotMargin] = useState(defaultSpotMargin)
2021-09-25 05:41:04 -07:00
const [positionSizePercent, setPositionSizePercent] = useState('')
const [insufficientSol, setInsufficientSol] = useState(false)
const { takerFee, makerFee } = useFees()
2021-10-18 13:38:03 -07:00
const { totalMsrm } = useSrmAccount()
2021-09-25 05:41:04 -07:00
const mangoGroup = useMangoStore((s) => s.selectedMangoGroup.current)
const mangoCache = useMangoStore((s) => s.selectedMangoGroup.cache)
const marketIndex = getMarketIndexBySymbol(
groupConfig,
marketConfig.baseSymbol
)
2021-10-01 09:21:09 -07:00
let perpAccount
if (isPerpMarket && mangoAccount) {
perpAccount = mangoAccount.perpAccounts[marketIndex]
}
2021-09-26 06:20:51 -07:00
const { width } = useViewport()
const isMobile = width ? width < breakpoints.sm : false
2021-09-24 05:24:17 -07:00
const {
side,
baseSize,
quoteSize,
price,
tradeType,
triggerPrice,
2021-09-24 11:58:00 -07:00
triggerCondition,
2021-09-24 05:24:17 -07:00
} = useMangoStore((s) => s.tradeForm)
2021-09-24 11:58:00 -07:00
const isLimitOrder = ['Limit', 'Stop Limit', 'Take Profit Limit'].includes(
tradeType
)
const isMarketOrder = ['Market', 'Stop Loss', 'Take Profit'].includes(
tradeType
)
2021-09-30 07:21:45 -07:00
const isTriggerLimit = ['Stop Limit', 'Take Profit Limit'].includes(tradeType)
const isTriggerOrder = TRIGGER_ORDER_TYPES.includes(tradeType)
2021-09-24 05:24:17 -07:00
2021-12-02 20:30:59 -08:00
// TODO saml - create a tick box on the UI; Only available on perps
// eslint-disable-next-line
const [postOnlySlide, setPostOnlySlide] = useState(false)
2021-09-24 05:24:17 -07:00
const [postOnly, setPostOnly] = useState(false)
const [ioc, setIoc] = useState(false)
const orderBookRef = useRef(useMangoStore.getState().selectedMarket.orderBook)
const orderbook = orderBookRef.current
2021-12-03 02:34:10 -08:00
const [maxSlippage, setMaxSlippage] = useLocalStorageStringState(
MAX_SLIPPAGE_KEY,
2021-12-03 02:39:10 -08:00
'0.025'
2021-12-03 02:34:10 -08:00
)
const [maxSlippagePercentage, setMaxSlippagePercentage] = useState(
2021-12-03 09:31:22 -08:00
clamp(parseFloat(maxSlippage), 0, 1) * 100
2021-12-03 02:34:10 -08:00
)
const [editMaxSlippage, setEditMaxSlippage] = useState(false)
2021-12-14 02:03:54 -08:00
const [showCustomSlippageForm, setShowCustomSlippageForm] = useState(false)
const slippagePresets = ['1', '1.5', '2', '2.5', '3']
2021-12-03 02:34:10 -08:00
const saveMaxSlippage = (slippage) => {
2021-12-03 09:31:22 -08:00
setMaxSlippage(clamp(slippage / 100, 0, 1).toString())
2021-12-03 02:34:10 -08:00
setEditMaxSlippage(false)
}
2021-09-24 05:24:17 -07:00
useEffect(
() =>
useMangoStore.subscribe(
// @ts-ignore
(orderBook) => (orderBookRef.current = orderBook),
(state) => state.selectedMarket.orderBook
),
[]
)
useEffect(() => {
const walletSol = walletTokens.find((a) => a.config.symbol === 'SOL')
walletSol ? setInsufficientSol(walletSol.uiBalance < 0.01) : null
}, [walletTokens])
2021-09-24 05:24:17 -07:00
useEffect(() => {
if (tradeType === 'Market') {
set((s) => {
s.tradeForm.price = ''
})
}
}, [tradeType, set])
2021-09-24 11:58:00 -07:00
useEffect(() => {
let condition
switch (tradeType) {
case 'Stop Loss':
case 'Stop Limit':
condition = side == 'buy' ? 'above' : 'below'
break
case 'Take Profit':
case 'Take Profit Limit':
condition = side == 'buy' ? 'below' : 'above'
break
}
if (condition) {
set((s) => {
s.tradeForm.triggerCondition = condition
})
}
}, [set, tradeType, side])
const { max, deposits, borrows, spotMax, reduceMax } = useMemo(() => {
2021-09-25 05:41:04 -07:00
if (!mangoAccount) return { max: 0 }
const priceOrDefault = price
? I80F48.fromNumber(price)
: mangoGroup.getPrice(marketIndex, mangoCache)
let spotMax
if (marketConfig.kind === 'spot') {
const token =
side === 'buy'
? getTokenBySymbol(groupConfig, 'USDC')
: getTokenBySymbol(groupConfig, marketConfig.baseSymbol)
const tokenIndex = mangoGroup.getTokenIndex(token.mintKey)
const availableBalance = floorToDecimal(
nativeI80F48ToUi(
mangoAccount.getAvailableBalance(mangoGroup, mangoCache, tokenIndex),
token.decimals
).toNumber(),
token.decimals
)
spotMax =
side === 'buy'
? availableBalance / priceOrDefault.toNumber()
: availableBalance
}
2021-09-25 05:41:04 -07:00
const {
max: maxQuote,
deposits,
borrows,
} = mangoAccount.getMaxLeverageForMarket(
mangoGroup,
mangoCache,
marketIndex,
market,
side,
priceOrDefault
)
let reduceMax
2022-01-13 08:38:55 -08:00
if (market && market instanceof PerpMarket) {
reduceMax =
2022-01-13 08:38:55 -08:00
Math.abs(market?.baseLotsToNumber(perpAccount?.basePosition)) || 0
} else {
reduceMax = 0
}
2021-09-25 05:41:04 -07:00
if (maxQuote.toNumber() <= 0) return { max: 0 }
// multiply the maxQuote by a scaler value to account for
// srm fees or rounding issues in getMaxLeverageForMarket
const maxScaler = market instanceof PerpMarket ? 0.99 : 0.95
const scaledMax = price
? (maxQuote.toNumber() * maxScaler) / price
: (maxQuote.toNumber() * maxScaler) /
mangoGroup.getPrice(marketIndex, mangoCache).toNumber()
return { max: scaledMax, deposits, borrows, spotMax, reduceMax }
2022-01-12 14:46:41 -08:00
}, [
mangoAccount,
mangoGroup,
mangoCache,
marketIndex,
market,
side,
price,
reduceOnly,
])
2021-09-25 05:41:04 -07:00
const onChangeSide = (side) => {
setPositionSizePercent('')
2021-09-24 05:24:17 -07:00
set((s) => {
s.tradeForm.side = side
})
2021-09-24 11:58:00 -07:00
}
2021-09-24 05:24:17 -07:00
const setBaseSize = (baseSize) =>
set((s) => {
if (!Number.isNaN(parseFloat(baseSize))) {
s.tradeForm.baseSize = parseFloat(baseSize)
} else {
s.tradeForm.baseSize = baseSize
}
})
const setQuoteSize = (quoteSize) =>
set((s) => {
if (!Number.isNaN(parseFloat(quoteSize))) {
s.tradeForm.quoteSize = parseFloat(quoteSize)
} else {
s.tradeForm.quoteSize = quoteSize
}
})
const setPrice = (price) =>
set((s) => {
if (!Number.isNaN(parseFloat(price))) {
s.tradeForm.price = parseFloat(price)
} else {
s.tradeForm.price = price
}
})
2021-09-24 11:58:00 -07:00
const setTradeType = (type) => {
2021-09-24 05:24:17 -07:00
set((s) => {
s.tradeForm.tradeType = type
})
2021-09-24 11:58:00 -07:00
}
2021-09-24 05:24:17 -07:00
const setTriggerPrice = (price) => {
set((s) => {
if (!Number.isNaN(parseFloat(price))) {
s.tradeForm.triggerPrice = parseFloat(price)
} else {
s.tradeForm.triggerPrice = price
}
})
if (isMarketOrder) {
onSetPrice(price)
}
}
const markPriceRef = useRef(useMangoStore.getState().selectedMarket.markPrice)
const markPrice = markPriceRef.current
useEffect(
() =>
useMangoStore.subscribe(
(markPrice) => (markPriceRef.current = markPrice as number),
(state) => state.selectedMarket.markPrice
),
[]
)
let minOrderSize = '0'
if (market instanceof Market && market.minOrderSize) {
minOrderSize = market.minOrderSize.toString()
} else if (market instanceof PerpMarket) {
2021-10-29 11:57:50 -07:00
const baseDecimals = marketConfig.baseDecimals
2021-09-24 05:24:17 -07:00
minOrderSize = new Big(market.baseLotSize)
.div(new Big(10).pow(baseDecimals))
.toString()
}
const sizeDecimalCount = getDecimalCount(minOrderSize)
let tickSize = 1
if (market instanceof Market) {
tickSize = market.tickSize
} else if (isPerpMarket) {
2021-10-29 11:57:50 -07:00
const baseDecimals = marketConfig.baseDecimals
const quoteDecimals = marketConfig.quoteDecimals
2021-09-24 05:24:17 -07:00
const nativeToUi = new Big(10).pow(baseDecimals - quoteDecimals)
const lotsToNative = new Big(market.quoteLotSize).div(
new Big(market.baseLotSize)
)
tickSize = lotsToNative.mul(nativeToUi).toNumber()
}
const onSetPrice = (price: number | '') => {
setPrice(price)
if (!price) return
if (baseSize) {
onSetBaseSize(baseSize)
}
}
const onSetBaseSize = (baseSize: number | '') => {
const { price } = useMangoStore.getState().tradeForm
setBaseSize(baseSize)
if (!baseSize) {
setQuoteSize('')
return
}
const usePrice = Number(price) || markPrice
if (!usePrice) {
setQuoteSize('')
return
}
const rawQuoteSize = baseSize * usePrice
setQuoteSize(rawQuoteSize.toFixed(6))
setPositionSizePercent('')
2021-09-24 05:24:17 -07:00
}
const onSetQuoteSize = (quoteSize: number | '') => {
setQuoteSize(quoteSize)
if (!quoteSize) {
setBaseSize('')
return
}
if (!Number(price) && isLimitOrder) {
setBaseSize('')
return
}
const usePrice = Number(price) || markPrice
const rawBaseSize = quoteSize / usePrice
const baseSize = quoteSize && floorToDecimal(rawBaseSize, sizeDecimalCount)
setBaseSize(baseSize)
setPositionSizePercent('')
2021-09-24 05:24:17 -07:00
}
const onTradeTypeChange = (tradeType) => {
setTradeType(tradeType)
setPostOnly(false)
setReduceOnly(false)
if (TRIGGER_ORDER_TYPES.includes(tradeType)) {
setReduceOnly(true)
}
2021-09-24 11:58:00 -07:00
if (['Market', 'Stop Loss', 'Take Profit'].includes(tradeType)) {
2021-09-24 05:24:17 -07:00
setIoc(true)
if (isTriggerOrder) {
setPrice(triggerPrice)
}
} else {
const priceOnBook = side === 'buy' ? orderbook?.asks : orderbook?.bids
if (priceOnBook && priceOnBook.length > 0 && priceOnBook[0].length > 0) {
setPrice(priceOnBook[0][0])
}
setIoc(false)
}
}
2021-12-02 20:30:59 -08:00
// TODO saml - use
// eslint-disable-next-line
const postOnlySlideOnChange = (checked) => {
if (checked) {
setIoc(false)
setPostOnly(false)
}
setPostOnlySlide(checked)
}
2021-09-24 05:24:17 -07:00
const postOnChange = (checked) => {
if (checked) {
setIoc(false)
2021-12-02 20:30:59 -08:00
setPostOnlySlide(false)
2021-09-24 05:24:17 -07:00
}
setPostOnly(checked)
}
const iocOnChange = (checked) => {
if (checked) {
setPostOnly(false)
2021-12-02 20:30:59 -08:00
setPostOnlySlide(false)
2021-09-24 05:24:17 -07:00
}
setIoc(checked)
}
const reduceOnChange = (checked) => {
handleSetPositionSize(positionSizePercent, spotMargin, checked)
2021-09-24 05:24:17 -07:00
setReduceOnly(checked)
}
const marginOnChange = (checked) => {
setSpotMargin(checked)
if (positionSizePercent) {
handleSetPositionSize(positionSizePercent, checked, reduceOnly)
}
}
2021-09-24 05:24:17 -07:00
const handleSetPositionSize = (percent, spotMargin, reduceOnly) => {
2021-09-25 05:41:04 -07:00
setPositionSizePercent(percent)
2022-01-12 14:46:41 -08:00
const baseSizeMax = reduceOnly
? reduceMax
: spotMargin || marketConfig.kind === 'perp'
? max
: spotMax
const baseSize = baseSizeMax * (parseInt(percent) / 100)
2021-09-25 05:41:04 -07:00
const step = parseFloat(minOrderSize)
const roundedSize = (Math.floor(baseSize / step) * step).toFixed(
2021-09-25 05:41:04 -07:00
sizeDecimalCount
)
setBaseSize(parseFloat(roundedSize))
const usePrice = Number(price) || markPrice
if (!usePrice) {
setQuoteSize('')
}
const rawQuoteSize = parseFloat(roundedSize) * usePrice
setQuoteSize(rawQuoteSize.toFixed(6))
}
const percentToClose = (size, total) => {
if (!size || !total) return 0
2021-09-25 05:41:04 -07:00
return (size / total) * 100
}
const roundedDeposits = parseFloat(deposits?.toFixed(sizeDecimalCount))
const roundedBorrows = parseFloat(borrows?.toFixed(sizeDecimalCount))
const closeDepositString =
percentToClose(baseSize, roundedDeposits) > 100
? t('close-open-short', {
size: (+baseSize - roundedDeposits).toFixed(sizeDecimalCount),
symbol: marketConfig.baseSymbol,
})
: `${percentToClose(baseSize, roundedDeposits).toFixed(0)}% ${t(
'close-position'
).toLowerCase()}`
2021-09-25 05:41:04 -07:00
const closeBorrowString =
percentToClose(baseSize, roundedBorrows) > 100
? t('close-open-long', {
size: (+baseSize - roundedDeposits).toFixed(sizeDecimalCount),
symbol: marketConfig.baseSymbol,
})
: `${percentToClose(baseSize, roundedBorrows).toFixed(0)}% ${t(
'close-position'
).toLowerCase()}`
2021-09-25 05:41:04 -07:00
// The reference price is the book mid if book is double sided; else mark price
const bb = orderbook?.bids?.length > 0 && Number(orderbook.bids[0][0])
const ba = orderbook?.asks?.length > 0 && Number(orderbook.asks[0][0])
const referencePrice = bb && ba ? (bb + ba) / 2 : markPrice
2021-10-05 12:12:26 -07:00
let priceImpact
2021-10-01 08:32:15 -07:00
let estimatedPrice = price
2021-12-03 02:34:10 -08:00
if (tradeType === 'Market' && baseSize > 0) {
2021-10-01 08:32:15 -07:00
const estimateMarketPrice = (
orderBook: Orderbook,
size: number,
side: 'buy' | 'sell'
): number => {
const orders = side === 'buy' ? orderBook.asks : orderBook.bids
let accSize = 0
let accPrice = 0
for (const [orderPrice, orderSize] of orders) {
const remainingSize = size - accSize
if (remainingSize <= orderSize) {
accSize += remainingSize
accPrice += remainingSize * orderPrice
break
}
accSize += orderSize
accPrice += orderSize * orderPrice
}
if (!accSize) {
console.log('Orderbook empty no market price available')
2021-10-01 08:32:15 -07:00
return markPrice
}
return accPrice / accSize
}
2021-10-01 09:21:09 -07:00
const estimatedSize =
2022-01-12 14:46:41 -08:00
perpAccount && reduceOnly && market instanceof PerpMarket
2022-01-13 08:38:55 -08:00
? Math.abs(market.baseLotsToNumber(perpAccount.basePosition))
2021-10-01 09:21:09 -07:00
: baseSize
estimatedPrice = estimateMarketPrice(orderbook, estimatedSize || 0, side)
2021-10-01 09:21:09 -07:00
const slippageAbs =
estimatedSize > 0 ? Math.abs(estimatedPrice - referencePrice) : 0
const slippageRel = slippageAbs / referencePrice
2021-10-01 08:32:15 -07:00
2021-10-01 09:21:09 -07:00
const takerFeeRel = takerFee
const takerFeeAbs = estimatedSize
? takerFeeRel * estimatedPrice * estimatedSize
: 0
2021-10-01 08:32:15 -07:00
priceImpact = {
slippage: [slippageAbs, slippageRel],
takerFee: [takerFeeAbs, takerFeeRel],
}
}
2021-09-24 05:24:17 -07:00
async function onSubmit() {
if (!price && isLimitOrder) {
notify({
title: t('missing-price'),
2021-09-24 05:24:17 -07:00
type: 'error',
})
return
} else if (!baseSize) {
notify({
title: t('missing-size'),
2021-09-24 05:24:17 -07:00
type: 'error',
})
return
} else if (!triggerPrice && isTriggerOrder) {
notify({
title: t('missing-trigger'),
2021-09-24 05:24:17 -07:00
type: 'error',
})
return
}
const mangoAccount = useMangoStore.getState().selectedMangoAccount.current
const mangoGroup = useMangoStore.getState().selectedMangoGroup.current
const askInfo =
useMangoStore.getState().accountInfos[marketConfig.asksKey.toString()]
const bidInfo =
useMangoStore.getState().accountInfos[marketConfig.bidsKey.toString()]
2021-09-24 05:24:17 -07:00
const wallet = useMangoStore.getState().wallet.current
2022-02-14 09:17:11 -08:00
const referrerPk = useMangoStore.getState().referrerPk
2021-09-24 05:24:17 -07:00
if (!wallet || !mangoGroup || !mangoAccount || !market) return
try {
const orderPrice = calculateTradePrice(
marketConfig.kind,
2021-09-24 05:24:17 -07:00
tradeType,
orderbook,
baseSize,
side,
price || markPrice,
2021-09-24 05:24:17 -07:00
triggerPrice
)
if (!orderPrice) {
notify({
title: t('price-unavailable'),
description: t('try-again'),
2021-09-24 05:24:17 -07:00
type: 'error',
})
}
// TODO: this has a race condition when switching between markets or buy & sell
// spot market orders will sometimes not be ioc but limit
2021-09-24 05:24:17 -07:00
const orderType = ioc ? 'ioc' : postOnly ? 'postOnly' : 'limit'
console.log(
'submit',
side,
baseSize.toString(),
orderPrice.toString(),
orderType,
market instanceof Market && 'spot',
isTriggerOrder && 'trigger'
)
2021-09-24 05:24:17 -07:00
let txid
if (market instanceof Market) {
txid = await mangoClient.placeSpotOrder2(
mangoGroup,
mangoAccount,
market,
wallet,
side,
orderPrice,
baseSize,
2021-10-18 13:38:03 -07:00
orderType,
null,
totalMsrm > 0
2021-09-24 05:24:17 -07:00
)
actions.reloadOrders()
2021-09-24 05:24:17 -07:00
} else {
let perpOrderType: PerpOrderType = orderType
let perpOrderPrice: number = orderPrice
if (isMarketOrder) {
if (tradeType === 'Market' && maxSlippage !== undefined) {
perpOrderType = 'ioc'
if (side === 'buy') {
2021-12-03 02:34:10 -08:00
perpOrderPrice = markPrice * (1 + parseFloat(maxSlippage))
} else {
2021-12-03 09:31:22 -08:00
perpOrderPrice = Math.max(
market.tickSize,
markPrice * (1 - parseFloat(maxSlippage))
)
}
} else {
perpOrderType = 'market'
}
}
2021-09-24 05:24:17 -07:00
if (isTriggerOrder) {
txid = await mangoClient.addPerpTriggerOrder(
mangoGroup,
mangoAccount,
market,
wallet,
perpOrderType,
2021-09-24 05:24:17 -07:00
side,
perpOrderPrice,
2021-09-24 05:24:17 -07:00
baseSize,
2021-09-24 11:58:00 -07:00
triggerCondition,
Number(triggerPrice),
true // reduceOnly
2021-09-24 05:24:17 -07:00
)
actions.reloadOrders()
2021-09-24 05:24:17 -07:00
} else {
txid = await mangoClient.placePerpOrder(
mangoGroup,
mangoAccount,
mangoGroup.mangoCache,
market,
wallet,
side,
perpOrderPrice,
2021-09-24 05:24:17 -07:00
baseSize,
perpOrderType,
2021-09-24 11:58:00 -07:00
Date.now(),
2021-09-24 05:24:17 -07:00
side === 'buy' ? askInfo : bidInfo, // book side used for ConsumeEvents
2022-02-14 09:17:11 -08:00
reduceOnly,
referrerPk
2021-09-24 05:24:17 -07:00
)
}
}
if (txid instanceof Array) {
for (const [index, id] of txid.entries()) {
notify({
title:
index === 0 ? 'Transaction successful' : t('successfully-placed'),
txid: id,
})
}
} else {
notify({ title: t('successfully-placed'), txid })
}
2021-09-24 05:24:17 -07:00
setPrice('')
onSetBaseSize('')
} catch (e) {
notify({
title: t('order-error'),
2021-09-24 05:24:17 -07:00
description: e.message,
txid: e.txid,
type: 'error',
})
console.error(e)
2021-09-24 05:24:17 -07:00
} finally {
actions.reloadMangoAccount()
actions.loadMarketFills()
}
}
// const showReduceOnly = (basePosition: number) => {
2021-12-03 07:45:29 -08:00
// return (
// (basePosition > 0 && side === 'sell') ||
// (basePosition < 0 && side === 'buy')
// )
// }
2021-11-12 07:39:58 -08:00
/*
const roundedMax = (
Math.round(max / parseFloat(minOrderSize)) * parseFloat(minOrderSize)
).toFixed(sizeDecimalCount)
2021-11-12 07:39:58 -08:00
*/
2021-11-12 07:39:58 -08:00
const sizeTooLarge = false /*
spotMargin || marketConfig.kind === 'perp'
? baseSize > roundedMax
2021-11-12 07:39:58 -08:00
: baseSize > spotMax*/
2021-09-24 05:24:17 -07:00
const disabledTradeButton =
(!price && isLimitOrder) ||
!baseSize ||
!connected ||
!mangoAccount ||
2021-12-03 02:34:10 -08:00
sizeTooLarge ||
editMaxSlippage
2021-09-24 05:24:17 -07:00
2021-11-22 15:54:51 -08:00
const canTrade = ipAllowed || (market instanceof Market && spotAllowed)
// If stop loss or take profit, walk up the book and alert user if slippage will be high
let warnUserSlippage = false
if (isMarketOrder && isTriggerOrder) {
const bookSide = side === 'buy' ? orderbook.asks : orderbook.bids
let base = 0
let quote = 0
for (const [p, q] of bookSide) {
base += q
quote += p * q
if (base >= baseSize) {
break
}
}
if (base < baseSize || (baseSize && base === 0)) {
warnUserSlippage = true
} else if (baseSize > 0) {
// only check if baseSize nonzero because this implies base nonzero
const avgPrice = quote / base
warnUserSlippage = Math.abs(avgPrice / referencePrice - 1) > 0.025
}
}
2021-09-26 06:20:51 -07:00
return (
2021-09-25 05:41:04 -07:00
<div className="flex flex-col h-full">
2021-09-26 06:20:51 -07:00
<ElementTitle className="hidden md:flex">
2021-09-25 02:40:35 -07:00
{marketConfig.name}
2021-09-24 05:24:17 -07:00
<span className="border border-th-primary ml-2 px-1 py-0.5 rounded text-xs text-th-primary">
{initLeverage}x
</span>
</ElementTitle>
2021-12-03 02:34:10 -08:00
{insufficientSol ? (
<div className="pb-3 text-left">
2021-12-21 11:56:14 -08:00
<InlineNotification desc={t('add-more-sol')} type="warning" />
2021-12-03 02:34:10 -08:00
</div>
) : null}
<OrderSideTabs onChange={onChangeSide} side={side} />
2022-01-31 07:52:28 -08:00
<div className="grid grid-cols-12 gap-x-1.5 gap-y-0.5 text-left">
2021-09-30 07:21:45 -07:00
<div className="col-span-12 md:col-span-6">
<label className="text-xxs text-th-fgd-3">{t('type')}</label>
2021-09-25 05:41:04 -07:00
<TradeType
onChange={onTradeTypeChange}
value={tradeType}
offerTriggers={isPerpMarket}
/>
</div>
2021-09-30 07:21:45 -07:00
<div className="col-span-12 md:col-span-6">
{!isTriggerOrder ? (
<>
<label className="text-xxs text-th-fgd-3">{t('price')}</label>
2021-09-30 07:21:45 -07:00
<Input
type="number"
min="0"
step={tickSize}
onChange={(e) => onSetPrice(e.target.value)}
value={price}
disabled={isMarketOrder}
placeholder={tradeType === 'Market' ? markPrice : null}
prefix={
<img
src={`/assets/icons/${groupConfig.quoteSymbol.toLowerCase()}.svg`}
width="16"
height="16"
/>
}
2021-09-25 05:41:04 -07:00
/>
2021-09-30 07:21:45 -07:00
</>
) : (
<>
<label className="text-xxs text-th-fgd-3">
{t('trigger-price')}
</label>
2021-09-25 05:41:04 -07:00
<Input
type="number"
min="0"
step={tickSize}
onChange={(e) => setTriggerPrice(e.target.value)}
value={triggerPrice}
prefix={
<img
src={`/assets/icons/${groupConfig.quoteSymbol.toLowerCase()}.svg`}
width="16"
height="16"
/>
}
/>
2021-09-30 07:21:45 -07:00
</>
)}
</div>
{isTriggerLimit && (
<>
<div className="col-span-12">
2021-12-03 14:36:34 -08:00
<label className="text-xxs text-th-fgd-3">
{t('limit-price')}
</label>
2021-09-30 07:21:45 -07:00
<Input
type="number"
min="0"
step={tickSize}
onChange={(e) => onSetPrice(e.target.value)}
value={price}
prefix={
<img
src={`/assets/icons/${groupConfig.quoteSymbol.toLowerCase()}.svg`}
width="16"
height="16"
/>
}
/>
2021-09-25 05:41:04 -07:00
</div>
</>
)}
2021-09-30 07:21:45 -07:00
<div className="col-span-6">
<label className="text-xxs text-th-fgd-3">{t('size')}</label>
2021-09-30 07:21:45 -07:00
<Input
type="number"
min="0"
step={minOrderSize}
onChange={(e) => onSetBaseSize(e.target.value)}
value={baseSize}
prefix={
<img
src={`/assets/icons/${marketConfig.baseSymbol.toLowerCase()}.svg`}
width="16"
height="16"
/>
}
/>
2021-09-25 05:41:04 -07:00
</div>
2021-09-30 07:21:45 -07:00
<div className="col-span-6">
<label className="text-xxs text-th-fgd-3">{t('quantity')}</label>
2021-09-30 07:21:45 -07:00
<Input
type="number"
min="0"
step={minOrderSize}
onChange={(e) => onSetQuoteSize(e.target.value)}
value={quoteSize}
prefix={
<img
src={`/assets/icons/${groupConfig.quoteSymbol.toLowerCase()}.svg`}
width="16"
height="16"
/>
}
/>
2021-09-25 05:41:04 -07:00
</div>
2022-01-31 07:52:28 -08:00
<div className="col-span-12 mt-1">
2021-09-25 05:41:04 -07:00
<ButtonGroup
activeValue={positionSizePercent}
onChange={(p) => handleSetPositionSize(p, spotMargin, reduceOnly)}
2021-09-25 05:41:04 -07:00
unit="%"
2021-09-26 06:20:51 -07:00
values={
isMobile
? ['10', '25', '50', '100']
2021-09-26 06:20:51 -07:00
: ['10', '25', '50', '75', '100']
}
2021-09-24 11:58:00 -07:00
/>
{marketConfig.kind === 'perp' ? (
side === 'sell' ? (
roundedDeposits > 0 ? (
<div className="text-th-fgd-4 text-xs tracking-normal mt-2">
<span>{closeDepositString}</span>
</div>
) : null
) : roundedBorrows > 0 ? (
<div className="text-th-fgd-4 text-xs tracking-normal mt-2">
<span>{closeBorrowString}</span>
2021-09-26 06:20:51 -07:00
</div>
) : null
) : null}
<div className="sm:flex">
2021-09-25 05:41:04 -07:00
{isLimitOrder ? (
2021-09-26 06:20:51 -07:00
<div className="flex">
2022-01-31 07:52:28 -08:00
<div className="mr-4 mt-3">
2021-09-25 05:41:04 -07:00
<Tooltip
2021-09-26 06:20:51 -07:00
className="hidden md:block"
2021-09-25 05:41:04 -07:00
delay={250}
placement="left"
content={t('tooltip-post')}
2021-09-25 05:41:04 -07:00
>
2021-09-26 06:20:51 -07:00
<Checkbox
2021-09-25 05:41:04 -07:00
checked={postOnly}
2021-09-26 06:20:51 -07:00
onChange={(e) => postOnChange(e.target.checked)}
2021-09-25 05:41:04 -07:00
>
POST
2021-09-26 06:20:51 -07:00
</Checkbox>
2021-09-25 05:41:04 -07:00
</Tooltip>
</div>
2022-01-31 07:52:28 -08:00
<div className="mr-4 mt-3">
2021-09-25 05:41:04 -07:00
<Tooltip
2021-09-26 06:20:51 -07:00
className="hidden md:block"
2021-09-25 05:41:04 -07:00
delay={250}
placement="left"
content={t('tooltip-ioc')}
2021-09-25 05:41:04 -07:00
>
2021-09-26 06:20:51 -07:00
<div className="flex items-center text-th-fgd-3 text-xs">
<Checkbox
checked={ioc}
onChange={(e) => iocOnChange(e.target.checked)}
>
IOC
</Checkbox>
</div>
2021-09-25 05:41:04 -07:00
</Tooltip>
</div>
2021-09-26 06:20:51 -07:00
</div>
2021-09-25 05:41:04 -07:00
) : null}
{/*
Add the following line to the ternary below once we are
auto updating the reduceOnly state when doing a market order:
&& showReduceOnly(perpAccount?.basePosition.toNumber())
*/}
{marketConfig.kind === 'perp' ? (
2022-01-31 07:52:28 -08:00
<div className="mt-3">
2021-09-26 06:20:51 -07:00
<Tooltip
className="hidden md:block"
delay={250}
placement="left"
content={t('tooltip-reduce')}
2021-09-25 05:41:04 -07:00
>
2021-09-26 06:20:51 -07:00
<Checkbox
checked={reduceOnly}
onChange={(e) => reduceOnChange(e.target.checked)}
disabled={isTriggerOrder}
2021-09-26 06:20:51 -07:00
>
Reduce Only
</Checkbox>
</Tooltip>
</div>
2021-09-25 05:41:04 -07:00
) : null}
{marketConfig.kind === 'spot' ? (
2022-01-31 07:52:28 -08:00
<div className="mt-3">
<Tooltip
delay={250}
placement="left"
content={t('tooltip-enable-margin')}
>
<Checkbox
checked={spotMargin}
onChange={(e) => marginOnChange(e.target.checked)}
>
2022-01-18 12:48:46 -08:00
{t('margin')}
</Checkbox>
</Tooltip>
</div>
) : null}
2021-09-24 05:24:17 -07:00
</div>
{warnUserSlippage ? (
<div className="text-th-red flex items-center mt-1">
<div>
<ExclamationIcon className="h-5 w-5 mr-2" />
</div>
2022-01-18 12:48:46 -08:00
<div className="text-xs">{t('slippage-warning')}</div>
</div>
) : null}
2022-01-31 07:52:28 -08:00
<div className={`flex mt-3`}>
2021-11-22 15:54:51 -08:00
{canTrade ? (
2021-09-25 05:41:04 -07:00
<Button
disabled={disabledTradeButton}
onClick={onSubmit}
className={`bg-th-bkg-2 border ${
2021-09-25 05:41:04 -07:00
!disabledTradeButton
? side === 'buy'
? 'border-th-green hover:border-th-green-dark text-th-green hover:bg-th-green-dark'
: 'border-th-red hover:border-th-red-dark text-th-red hover:bg-th-red-dark'
2021-09-25 05:41:04 -07:00
: 'border border-th-bkg-4'
} hover:text-th-fgd-1 flex-grow`}
2021-09-25 05:41:04 -07:00
>
{sizeTooLarge
? t('too-large')
: side === 'buy'
? `${
baseSize > 0 ? `${t('buy')} ` + baseSize : `${t('buy')} `
} ${
isPerpMarket ? marketConfig.name : marketConfig.baseSymbol
}`
: `${
baseSize > 0
? `${t('sell')} ` + baseSize
: `${t('sell')} `
} ${
isPerpMarket ? marketConfig.name : marketConfig.baseSymbol
}`}
2021-09-25 05:41:04 -07:00
</Button>
2021-09-25 02:40:35 -07:00
) : (
<div className="flex-grow">
<Tooltip content={t('country-not-allowed-tooltip')}>
<div className="flex">
<Button disabled className="flex-grow">
<span>{t('country-not-allowed')}</span>
</Button>
</div>
</Tooltip>
</div>
2021-09-25 02:40:35 -07:00
)}
2021-09-25 05:41:04 -07:00
</div>
2021-12-03 02:34:10 -08:00
{tradeType === 'Market' && priceImpact ? (
<div className="col-span-12 md:col-span-10 md:col-start-3 mt-4">
{editMaxSlippage ? (
2021-12-14 02:03:54 -08:00
<div className="flex items-end">
<div className="w-full">
<div className="flex justify-between mb-1">
<div className="text-xs text-th-fgd-3">
{t('max-slippage')}
</div>
{!isMobile ? (
<LinkButton
className="font-normal text-xs"
onClick={() =>
setShowCustomSlippageForm(!showCustomSlippageForm)
}
>
{showCustomSlippageForm ? t('presets') : t('custom')}
</LinkButton>
) : null}
</div>
{showCustomSlippageForm || isMobile ? (
<Input
type="number"
min="0"
max="100"
onChange={(e) =>
setMaxSlippagePercentage(e.target.value)
}
suffix={
<div className="font-bold text-base text-th-fgd-3">
%
</div>
}
value={maxSlippagePercentage}
/>
) : (
<ButtonGroup
activeValue={maxSlippagePercentage.toString()}
className="h-10"
onChange={(p) => setMaxSlippagePercentage(p)}
unit="%"
values={slippagePresets}
/>
)}
2021-12-03 02:34:10 -08:00
</div>
2021-12-14 02:03:54 -08:00
<Button
className="h-10 ml-3"
onClick={() => saveMaxSlippage(maxSlippagePercentage)}
>
{t('save')}
</Button>
</div>
2021-12-03 02:34:10 -08:00
) : (
<>
{isPerpMarket ? (
<div className="flex justify-between mb-1 text-th-fgd-3 text-xs">
<div className="flex items-center">
2021-12-14 02:03:54 -08:00
{t('max-slippage')}
2022-01-18 12:48:46 -08:00
<Tooltip content={t('tooltip-slippage')}>
2021-12-03 02:34:10 -08:00
<div className="outline-none focus:outline-none">
<InformationCircleIcon className="h-4 w-4 ml-1.5 text-th-fgd-3" />
</div>
</Tooltip>
</div>
<div className="flex">
<span className="text-th-fgd-1">
{(parseFloat(maxSlippage) * 100).toFixed(2)}%
</span>
<LinkButton
className="ml-2 text-xs"
onClick={() => setEditMaxSlippage(true)}
>
{t('edit')}
</LinkButton>
</div>
</div>
) : null}
<EstPriceImpact priceImpact={priceImpact} />
</>
)}
</div>
2021-12-03 02:34:10 -08:00
) : (
<div className="flex flex-col md:flex-row text-xs text-th-fgd-4 px-6 mt-2.5 items-center justify-center">
<div>
{t('maker-fee')}: {(makerFee * 100).toFixed(2)}%{' '}
</div>
<span className="hidden md:block md:px-1">|</span>
<div>
{' '}
2021-12-08 09:27:48 -08:00
{t('taker-fee')}: {(takerFee * 100).toFixed(3)}%
2021-12-03 02:34:10 -08:00
</div>
</div>
2021-12-03 02:34:10 -08:00
)}
2021-09-25 05:41:04 -07:00
</div>
2021-09-24 05:24:17 -07:00
</div>
</div>
)
}