Merge pull request #182 from blockworks-foundation/trade-hot-keys

Trade Hot Keys
This commit is contained in:
saml33 2023-07-09 11:37:42 +10:00 committed by GitHub
commit f6b3d2c5e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1136 additions and 57 deletions

View File

@ -0,0 +1,494 @@
import ButtonGroup from '@components/forms/ButtonGroup'
import Checkbox from '@components/forms/Checkbox'
import Input from '@components/forms/Input'
import Label from '@components/forms/Label'
import Button, { IconButton } from '@components/shared/Button'
import InlineNotification from '@components/shared/InlineNotification'
import Modal from '@components/shared/Modal'
import { Table, Td, Th, TrBody, TrHead } from '@components/shared/TableElements'
import Tooltip from '@components/shared/Tooltip'
import { KeyIcon, TrashIcon } from '@heroicons/react/20/solid'
import useLocalStorageState from 'hooks/useLocalStorageState'
import { useTranslation } from 'next-i18next'
import { useState } from 'react'
import { ModalProps } from 'types/modal'
import { HOT_KEYS_KEY } from 'utils/constants'
export type HotKey = {
ioc: boolean
keySequence: string
margin: boolean
orderSide: 'buy' | 'sell'
orderSizeType: 'percentage' | 'notional'
orderSize: string
orderType: 'limit' | 'market'
orderPrice: string
postOnly: boolean
reduceOnly: boolean
}
const HotKeysSettings = () => {
const { t } = useTranslation(['common', 'settings', 'trade'])
const [hotKeys, setHotKeys] = useLocalStorageState(HOT_KEYS_KEY, [])
const [showHotKeyModal, setShowHotKeyModal] = useState(false)
const handleDeleteKey = (key: string) => {
const newKeys = hotKeys.filter((hk: HotKey) => hk.keySequence !== key)
setHotKeys([...newKeys])
}
return (
<>
<div className="mb-4 flex items-center justify-between">
<div className="pr-6">
<h2 className="mb-1 text-base">{t('settings:hot-keys')}</h2>
<p>{t('settings:hot-keys-desc')}</p>
</div>
{hotKeys.length ? (
<Button
className="whitespace-nowrap"
disabled={hotKeys.length >= 20}
onClick={() => setShowHotKeyModal(true)}
secondary
>
{t('settings:new-hot-key')}
</Button>
) : null}
</div>
{hotKeys.length === 20 ? (
<div className="mb-4">
<InlineNotification
type="warning"
desc={t('settings:error-key-limit-reached')}
/>
</div>
) : null}
{hotKeys.length ? (
<Table>
<thead>
<TrHead>
<Th className="text-left">{t('settings:key-sequence')}</Th>
<Th className="text-right">{t('trade:order-type')}</Th>
<Th className="text-right">{t('trade:side')}</Th>
<Th className="text-right">{t('trade:size')}</Th>
<Th className="text-right">{t('price')}</Th>
<Th className="text-right">{t('settings:options')}</Th>
<Th />
</TrHead>
</thead>
<tbody>
{hotKeys.map((hk: HotKey) => {
const {
keySequence,
orderSide,
orderPrice,
orderSize,
orderSizeType,
orderType,
ioc,
margin,
reduceOnly,
postOnly,
} = hk
const size =
orderSizeType === 'percentage'
? t('settings:percentage-of-max', { size: orderSize })
: `$${orderSize}`
const price = orderPrice
? `${orderPrice}% ${
orderSide === 'buy'
? t('settings:below')
: t('settings:above')
} oracle`
: t('trade:market')
const options = {
margin: margin,
IOC: ioc,
post: postOnly,
reduce: reduceOnly,
}
return (
<TrBody key={keySequence} className="text-right">
<Td className="text-left">{keySequence}</Td>
<Td className="text-right">{t(`trade:${orderType}`)}</Td>
<Td className="text-right">{t(orderSide)}</Td>
<Td className="text-right">{size}</Td>
<Td className="text-right">{price}</Td>
<Td className="text-right">
{Object.entries(options).map((e) => {
return e[1]
? `${e[0] !== 'margin' ? ', ' : ''}${t(
`trade:${e[0]}`
)}`
: ''
})}
</Td>
<Td>
<div className="flex justify-end">
<IconButton
onClick={() => handleDeleteKey(keySequence)}
size="small"
>
<TrashIcon className="h-4 w-4" />
</IconButton>
</div>
</Td>
</TrBody>
)
})}
</tbody>
</Table>
) : (
<div className="rounded-lg border border-th-bkg-3 p-6">
<div className="flex flex-col items-center">
<KeyIcon className="mb-2 h-6 w-6 text-th-fgd-4" />
<p className="mb-4">{t('settings:no-hot-keys')}</p>
<Button onClick={() => setShowHotKeyModal(true)}>
<div className="flex items-center">
{t('settings:new-hot-key')}
</div>
</Button>
</div>
</div>
)}
{showHotKeyModal ? (
<HotKeyModal
isOpen={showHotKeyModal}
onClose={() => setShowHotKeyModal(false)}
/>
) : null}
</>
)
}
export default HotKeysSettings
type FormErrors = Partial<Record<keyof HotKeyForm, string>>
type HotKeyForm = {
baseKey: string
triggerKey: string
price: string
side: 'buy' | 'sell'
size: string
sizeType: 'percentage' | 'notional'
orderType: 'limit' | 'market'
ioc: boolean
post: boolean
margin: boolean
reduce: boolean
}
const DEFAULT_FORM_VALUES: HotKeyForm = {
baseKey: 'shift',
triggerKey: '',
price: '',
side: 'buy',
size: '',
sizeType: 'percentage',
orderType: 'limit',
ioc: false,
post: false,
margin: false,
reduce: false,
}
const HotKeyModal = ({ isOpen, onClose }: ModalProps) => {
const { t } = useTranslation(['common', 'settings', 'trade'])
const [hotKeys, setHotKeys] = useLocalStorageState<HotKey[]>(HOT_KEYS_KEY, [])
const [hotKeyForm, setHotKeyForm] = useState<HotKeyForm>({
...DEFAULT_FORM_VALUES,
})
const [formErrors, setFormErrors] = useState<FormErrors>({})
const handleSetForm = (propertyName: string, value: string | boolean) => {
setFormErrors({})
setHotKeyForm((prevState) => ({ ...prevState, [propertyName]: value }))
}
const handlePostOnlyChange = (postOnly: boolean) => {
if (postOnly) {
handleSetForm('ioc', !postOnly)
}
handleSetForm('post', postOnly)
}
const handleIocChange = (ioc: boolean) => {
if (ioc) {
handleSetForm('post', !ioc)
}
handleSetForm('ioc', ioc)
}
const isFormValid = (form: HotKeyForm) => {
const invalidFields: FormErrors = {}
setFormErrors({})
const triggerKey: (keyof HotKeyForm)[] = ['triggerKey']
const requiredFields: (keyof HotKeyForm)[] = ['size', 'price', 'triggerKey']
const numberFields: (keyof HotKeyForm)[] = ['size', 'price']
const alphanumericRegex = /^[a-zA-Z0-9]+$/
for (const key of triggerKey) {
const value = form[key] as string
if (value.length > 1) {
invalidFields[key] = t('settings:error-too-many-characters')
}
if (!alphanumericRegex.test(value)) {
invalidFields[key] = t('settings:error-alphanumeric-only')
}
}
for (const key of requiredFields) {
const value = form[key] as string
if (!value) {
if (hotKeyForm.orderType === 'market') {
if (key !== 'price') {
invalidFields[key] = t('settings:error-required-field')
}
} else {
invalidFields[key] = t('settings:error-required-field')
}
}
}
for (const key of numberFields) {
const value = form[key] as string
if (value) {
if (isNaN(parseFloat(value))) {
invalidFields[key] = t('settings:error-must-be-number')
}
if (parseFloat(value) < 0) {
invalidFields[key] = t('settings:error-must-be-above-zero')
}
if (parseFloat(value) > 100) {
if (key === 'price') {
invalidFields[key] = t('settings:error-must-be-below-100')
} else {
if (hotKeyForm.sizeType === 'percentage') {
invalidFields[key] = t('settings:error-must-be-below-100')
}
}
}
}
}
const newKeySequence = `${form.baseKey}+${form.triggerKey}`
const keyExists = hotKeys.find((k) => k.keySequence === newKeySequence)
if (keyExists) {
invalidFields.triggerKey = t('settings:error-key-in-use')
}
if (Object.keys(invalidFields).length) {
setFormErrors(invalidFields)
}
return invalidFields
}
const handleSave = () => {
const invalidFields = isFormValid(hotKeyForm)
if (Object.keys(invalidFields).length) {
return
}
const newHotKey = {
keySequence: `${hotKeyForm.baseKey}+${hotKeyForm.triggerKey}`,
orderSide: hotKeyForm.side,
orderSizeType: hotKeyForm.sizeType,
orderSize: hotKeyForm.size,
orderType: hotKeyForm.orderType,
orderPrice: hotKeyForm.price,
ioc: hotKeyForm.ioc,
margin: hotKeyForm.margin,
postOnly: hotKeyForm.post,
reduceOnly: hotKeyForm.reduce,
}
setHotKeys([...hotKeys, newHotKey])
onClose()
}
return (
<Modal isOpen={isOpen} onClose={onClose}>
<>
<h2 className="mb-4 text-center">{t('settings:new-hot-key')}</h2>
<div className="mb-4">
<Label text={t('settings:base-key')} />
<ButtonGroup
activeValue={hotKeyForm.baseKey}
onChange={(key) => handleSetForm('baseKey', key)}
values={['shift', 'ctrl', 'option']}
/>
</div>
<div className="mb-4">
<Label text={t('settings:trigger-key')} />
<Input
hasError={formErrors.triggerKey !== undefined}
type="text"
value={hotKeyForm.triggerKey}
onChange={(e) =>
handleSetForm('triggerKey', e.target.value.toLowerCase())
}
/>
{formErrors.triggerKey ? (
<div className="mt-1">
<InlineNotification
type="error"
desc={formErrors.triggerKey}
hideBorder
hidePadding
/>
</div>
) : null}
</div>
<div className="mb-4">
<Label text={t('settings:order-side')} />
<ButtonGroup
activeValue={hotKeyForm.side}
names={[t('buy'), t('sell')]}
onChange={(side) => handleSetForm('side', side)}
values={['buy', 'sell']}
/>
</div>
<div className="mb-4">
<Label text={t('trade:order-type')} />
<ButtonGroup
activeValue={hotKeyForm.orderType}
names={[t('trade:limit'), t('market')]}
onChange={(type) => handleSetForm('orderType', type)}
values={['limit', 'market']}
/>
</div>
<div className="mb-4">
<Label text={t('settings:order-size-type')} />
<ButtonGroup
activeValue={hotKeyForm.sizeType}
names={[t('settings:percentage'), t('settings:notional')]}
onChange={(type) => handleSetForm('sizeType', type)}
values={['percentage', 'notional']}
/>
</div>
<div className="flex items-start space-x-4">
<div className="w-full">
<Tooltip
content={
hotKeyForm.sizeType === 'notional'
? t('settings:tooltip-hot-key-notional-size')
: t('settings:tooltip-hot-key-percentage-size')
}
>
<Label className="tooltip-underline" text={t('trade:size')} />
</Tooltip>
<Input
hasError={formErrors.size !== undefined}
type="text"
value={hotKeyForm.size}
onChange={(e) => handleSetForm('size', e.target.value)}
suffix={hotKeyForm.sizeType === 'percentage' ? '%' : 'USD'}
/>
{formErrors.size ? (
<div className="mt-1">
<InlineNotification
type="error"
desc={formErrors.size}
hideBorder
hidePadding
/>
</div>
) : null}
</div>
{hotKeyForm.orderType === 'limit' ? (
<div className="w-full">
<Tooltip content={t('settings:tooltip-hot-key-price')}>
<Label className="tooltip-underline" text={t('price')} />
</Tooltip>
<Input
hasError={formErrors.price !== undefined}
type="text"
value={hotKeyForm.price}
onChange={(e) => handleSetForm('price', e.target.value)}
placeholder="e.g. 1%"
suffix="%"
/>
{formErrors.price ? (
<div className="mt-1">
<InlineNotification
type="error"
desc={formErrors.price}
hideBorder
hidePadding
/>
</div>
) : null}
</div>
) : null}
</div>
<div className="flex flex-wrap md:flex-nowrap">
{hotKeyForm.orderType === 'limit' ? (
<div className="flex">
<div className="mr-3 mt-4" id="trade-step-six">
<Tooltip
className="hidden md:block"
delay={100}
content={t('trade:tooltip-post')}
>
<Checkbox
checked={hotKeyForm.post}
onChange={(e) => handlePostOnlyChange(e.target.checked)}
>
{t('trade:post')}
</Checkbox>
</Tooltip>
</div>
<div className="mr-3 mt-4" id="trade-step-seven">
<Tooltip
className="hidden md:block"
delay={100}
content={t('trade:tooltip-ioc')}
>
<div className="flex items-center text-xs text-th-fgd-3">
<Checkbox
checked={hotKeyForm.ioc}
onChange={(e) => handleIocChange(e.target.checked)}
>
IOC
</Checkbox>
</div>
</Tooltip>
</div>
</div>
) : null}
<div className="mt-4 mr-3" id="trade-step-eight">
<Tooltip
className="hidden md:block"
delay={100}
content={t('trade:tooltip-enable-margin')}
>
<Checkbox
checked={hotKeyForm.margin}
onChange={(e) => handleSetForm('margin', e.target.checked)}
>
{t('trade:margin')}
</Checkbox>
</Tooltip>
</div>
<div className="mr-3 mt-4">
<Tooltip
className="hidden md:block"
delay={100}
content={
'Reduce will only decrease the size of an open position. This is often used for closing a position.'
}
>
<div className="flex items-center text-xs text-th-fgd-3">
<Checkbox
checked={hotKeyForm.reduce}
onChange={(e) => handleSetForm('reduce', e.target.checked)}
>
{t('trade:reduce-only')}
</Checkbox>
</div>
</Tooltip>
</div>
</div>
<Button className="mt-6 w-full" onClick={handleSave}>
{t('settings:save-hot-key')}
</Button>
</>
</Modal>
)
}

View File

@ -42,7 +42,7 @@ const NotificationSettings = () => {
<h2 className="text-base">{t('settings:notifications')}</h2>
</div>
{isAuth ? (
<div className="flex items-center justify-between border-t border-th-bkg-3 p-4">
<div className="flex items-center justify-between border-y border-th-bkg-3 p-4">
<p>{t('settings:limit-order-filled')}</p>
<Switch
checked={!!data?.fillsNotifications}
@ -55,7 +55,7 @@ const NotificationSettings = () => {
/>
</div>
) : (
<div className="mb-8 rounded-lg border border-th-bkg-3 p-6">
<div className="rounded-lg border border-th-bkg-3 p-6">
{connected ? (
<div className="flex flex-col items-center">
<BellIcon className="mb-2 h-6 w-6 text-th-fgd-4" />

View File

@ -1,11 +1,16 @@
import { useViewport } from 'hooks/useViewport'
import AnimationSettings from './AnimationSettings'
import DisplaySettings from './DisplaySettings'
import HotKeysSettings from './HotKeysSettings'
import NotificationSettings from './NotificationSettings'
import PreferredExplorerSettings from './PreferredExplorerSettings'
import RpcSettings from './RpcSettings'
import SoundSettings from './SoundSettings'
import { breakpoints } from 'utils/theme'
const SettingsPage = () => {
const { width } = useViewport()
const isMobile = width ? width < breakpoints.lg : false
return (
<div className="grid grid-cols-12">
<div className="col-span-12 border-b border-th-bkg-3 lg:col-span-10 lg:col-start-2 xl:col-span-8 xl:col-start-3">
@ -14,9 +19,14 @@ const SettingsPage = () => {
<div className="col-span-12 border-b border-th-bkg-3 pt-8 lg:col-span-10 lg:col-start-2 xl:col-span-8 xl:col-start-3">
<DisplaySettings />
</div>
<div className="col-span-12 border-b border-th-bkg-3 pt-8 lg:col-span-10 lg:col-start-2 xl:col-span-8 xl:col-start-3">
<div className="col-span-12 pt-8 lg:col-span-10 lg:col-start-2 xl:col-span-8 xl:col-start-3">
<NotificationSettings />
</div>
{!isMobile ? (
<div className="col-span-12 pt-8 lg:col-span-10 lg:col-start-2 xl:col-span-8 xl:col-start-3">
<HotKeysSettings />
</div>
) : null}
<div className="col-span-12 border-b border-th-bkg-3 pt-8 lg:col-span-10 lg:col-start-2 xl:col-span-8 xl:col-start-3">
<AnimationSettings />
</div>

View File

@ -59,7 +59,7 @@ import InlineNotification from '@components/shared/InlineNotification'
const set = mangoStore.getState().set
const successSound = new Howl({
export const successSound = new Howl({
src: ['/sounds/swap-success.mp3'],
volume: 0.5,
})

View File

@ -16,6 +16,7 @@ import OrderbookAndTrades from './OrderbookAndTrades'
import FavoriteMarketsBar from './FavoriteMarketsBar'
import useLocalStorageState from 'hooks/useLocalStorageState'
import { TRADE_LAYOUT_KEY } from 'utils/constants'
import TradeHotKeys from './TradeHotKeys'
export type TradeLayout =
| 'chartLeft'
@ -206,7 +207,7 @@ const TradeAdvancedPage = () => {
return showMobileView ? (
<MobileTradeAdvancedPage />
) : (
<>
<TradeHotKeys>
<FavoriteMarketsBar />
<ResponsiveGridLayout
onBreakpointChange={(bp) => console.log('bp: ', bp)}
@ -262,7 +263,7 @@ const TradeAdvancedPage = () => {
{/* {!tourSettings?.trade_tour_seen && isOnboarded && connected ? (
<TradeOnboardingTour />
) : null} */}
</>
</TradeHotKeys>
)
}

View File

@ -0,0 +1,393 @@
import {
Group,
MangoAccount,
PerpMarket,
PerpOrderSide,
PerpOrderType,
Serum3Market,
Serum3OrderType,
Serum3SelfTradeBehavior,
Serum3Side,
} from '@blockworks-foundation/mango-v4'
import { HotKey } from '@components/settings/HotKeysSettings'
import mangoStore from '@store/mangoStore'
import { ReactNode, useCallback } from 'react'
import Hotkeys from 'react-hot-keys'
import { GenericMarket, isMangoError } from 'types'
import { HOT_KEYS_KEY, SOUND_SETTINGS_KEY } from 'utils/constants'
import { notify } from 'utils/notifications'
import { calculateLimitPriceForMarketOrder } from 'utils/tradeForm'
import { successSound } from './AdvancedTradeForm'
import useLocalStorageState from 'hooks/useLocalStorageState'
import { INITIAL_SOUND_SETTINGS } from '@components/settings/SoundSettings'
import useSelectedMarket from 'hooks/useSelectedMarket'
import { floorToDecimal, getDecimalCount } from 'utils/numbers'
import useMangoAccount from 'hooks/useMangoAccount'
import { Market } from '@project-serum/serum'
import { useRouter } from 'next/router'
import useUnownedAccount from 'hooks/useUnownedAccount'
import { useTranslation } from 'next-i18next'
const set = mangoStore.getState().set
const calcBaseSize = (
orderDetails: HotKey,
maxSize: number,
market: PerpMarket | Market,
oraclePrice: number,
quoteTokenIndex: number,
group: Group,
limitPrice?: number
) => {
const { orderSize, orderSide, orderSizeType, orderType } = orderDetails
let baseSize: number
let quoteSize: number
if (orderSide === 'buy') {
// assumes USDC = $1 as tokenIndex is 0
if (!quoteTokenIndex) {
quoteSize =
orderSizeType === 'percentage'
? (Number(orderSize) / 100) * maxSize
: Number(orderSize)
} else {
// required for non USDC quote tokens
const quoteBank = group.getFirstBankByTokenIndex(quoteTokenIndex)
const quotePrice = quoteBank.uiPrice
const orderSizeInQuote = Number(orderSize) / quotePrice
quoteSize =
orderSizeType === 'percentage'
? (orderSizeInQuote / 100) * maxSize
: orderSizeInQuote
}
if (orderType === 'market') {
baseSize = floorToDecimal(
quoteSize / oraclePrice,
getDecimalCount(market.minOrderSize)
).toNumber()
} else {
const price = limitPrice ? limitPrice : 0
baseSize = floorToDecimal(
quoteSize / price,
getDecimalCount(market.minOrderSize)
).toNumber()
}
} else {
if (orderSizeType === 'percentage') {
baseSize = floorToDecimal(
(Number(orderSize) / 100) * maxSize,
getDecimalCount(market.minOrderSize)
).toNumber()
} else {
if (orderType === 'market') {
baseSize = floorToDecimal(
Number(orderSize) / oraclePrice,
getDecimalCount(market.minOrderSize)
).toNumber()
} else {
const price = limitPrice ? limitPrice : 0
baseSize = floorToDecimal(
Number(orderSize) / price,
getDecimalCount(market.minOrderSize)
).toNumber()
}
}
}
return baseSize
}
const calcSpotMarketMax = (
mangoAccount: MangoAccount | undefined,
selectedMarket: GenericMarket | undefined,
side: string,
useMargin: boolean
) => {
const spotBalances = mangoStore.getState().mangoAccount.spotBalances
const group = mangoStore.getState().group
if (!mangoAccount || !group || !selectedMarket) return 0
if (!(selectedMarket instanceof Serum3Market)) return 0
let leverageMax = 0
let spotMax = 0
try {
if (side === 'buy') {
leverageMax = mangoAccount.getMaxQuoteForSerum3BidUi(
group,
selectedMarket.serumMarketExternal
)
const bank = group.getFirstBankByTokenIndex(
selectedMarket.quoteTokenIndex
)
const balance = mangoAccount.getTokenBalanceUi(bank)
const unsettled = spotBalances[bank.mint.toString()]?.unsettled || 0
spotMax = balance + unsettled
} else {
leverageMax = mangoAccount.getMaxBaseForSerum3AskUi(
group,
selectedMarket.serumMarketExternal
)
const bank = group.getFirstBankByTokenIndex(selectedMarket.baseTokenIndex)
const balance = mangoAccount.getTokenBalanceUi(bank)
const unsettled = spotBalances[bank.mint.toString()]?.unsettled || 0
spotMax = balance + unsettled
}
return useMargin ? leverageMax : Math.max(spotMax, 0)
} catch (e) {
console.error('Error calculating max size: ', e)
return 0
}
}
const calcPerpMax = (
mangoAccount: MangoAccount,
selectedMarket: GenericMarket,
side: string
) => {
const group = mangoStore.getState().group
if (
!mangoAccount ||
!group ||
!selectedMarket ||
selectedMarket instanceof Serum3Market
)
return 0
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: ', e)
return 0
}
}
const TradeHotKeys = ({ children }: { children: ReactNode }) => {
const { t } = useTranslation(['common', 'settings'])
const { price: oraclePrice, serumOrPerpMarket } = useSelectedMarket()
const { mangoAccountAddress } = useMangoAccount()
const { isUnownedAccount } = useUnownedAccount()
const { asPath } = useRouter()
const [hotKeys] = useLocalStorageState(HOT_KEYS_KEY, [])
const [soundSettings] = useLocalStorageState(
SOUND_SETTINGS_KEY,
INITIAL_SOUND_SETTINGS
)
const handlePlaceOrder = useCallback(
async (hkOrder: HotKey) => {
const client = mangoStore.getState().client
const group = mangoStore.getState().group
const mangoAccount = mangoStore.getState().mangoAccount.current
const actions = mangoStore.getState().actions
const selectedMarket = mangoStore.getState().selectedMarket.current
const {
ioc,
orderPrice,
orderSide,
orderType,
postOnly,
reduceOnly,
margin,
} = hkOrder
if (!group || !mangoAccount || !serumOrPerpMarket || !selectedMarket)
return
try {
const orderMax =
serumOrPerpMarket instanceof PerpMarket
? calcPerpMax(mangoAccount, selectedMarket, orderSide)
: calcSpotMarketMax(mangoAccount, selectedMarket, orderSide, margin)
const quoteTokenIndex =
selectedMarket instanceof PerpMarket
? 0
: selectedMarket.quoteTokenIndex
let baseSize: number
let price: number
if (orderType === 'market') {
baseSize = calcBaseSize(
hkOrder,
orderMax,
serumOrPerpMarket,
oraclePrice,
quoteTokenIndex,
group
)
const orderbook = mangoStore.getState().selectedMarket.orderbook
price = calculateLimitPriceForMarketOrder(
orderbook,
baseSize,
orderSide
)
} else {
// change in price from oracle for limit order
const priceChange = (Number(orderPrice) / 100) * oraclePrice
// subtract price change for buy limit, add for sell limit
const rawPrice =
orderSide === 'buy'
? oraclePrice - priceChange
: oraclePrice + priceChange
price = floorToDecimal(
rawPrice,
getDecimalCount(serumOrPerpMarket.tickSize)
).toNumber()
baseSize = calcBaseSize(
hkOrder,
orderMax,
serumOrPerpMarket,
oraclePrice,
quoteTokenIndex,
group,
price
)
}
// check if size < max
if (orderSide === 'buy') {
if (baseSize * price > orderMax) {
notify({
type: 'error',
title: t('settings:error-order-exceeds-max'),
})
return
}
} else {
console.log(baseSize, orderMax)
if (baseSize > orderMax) {
notify({
type: 'error',
title: t('settings:error-order-exceeds-max'),
})
return
}
}
notify({
type: 'info',
title: t('settings:placing-order'),
description: `${t(orderSide)} ${baseSize} ${selectedMarket.name} ${
orderType === 'limit'
? `${t('settings:at')} ${price}`
: `${t('settings:at')} ${t('market')}`
}`,
})
if (selectedMarket instanceof Serum3Market) {
const spotOrderType = ioc
? Serum3OrderType.immediateOrCancel
: postOnly && orderType !== 'market'
? Serum3OrderType.postOnly
: Serum3OrderType.limit
const tx = await client.serum3PlaceOrder(
group,
mangoAccount,
selectedMarket.serumMarketExternal,
orderSide === 'buy' ? Serum3Side.bid : Serum3Side.ask,
price,
baseSize,
Serum3SelfTradeBehavior.decrementTake,
spotOrderType,
Date.now(),
10
)
actions.fetchOpenOrders(true)
set((s) => {
s.successAnimation.trade = true
})
if (soundSettings['swap-success']) {
successSound.play()
}
notify({
type: 'success',
title: 'Transaction successful',
txid: tx,
})
} else if (selectedMarket instanceof PerpMarket) {
const perpOrderType =
orderType === 'market'
? PerpOrderType.market
: ioc
? PerpOrderType.immediateOrCancel
: postOnly
? PerpOrderType.postOnly
: PerpOrderType.limit
console.log('perpOrderType', perpOrderType)
const tx = await client.perpPlaceOrder(
group,
mangoAccount,
selectedMarket.perpMarketIndex,
orderSide === 'buy' ? PerpOrderSide.bid : PerpOrderSide.ask,
price,
Math.abs(baseSize),
undefined, // maxQuoteQuantity
Date.now(),
perpOrderType,
selectedMarket.reduceOnly || reduceOnly,
undefined,
undefined
)
actions.fetchOpenOrders(true)
set((s) => {
s.successAnimation.trade = true
})
if (soundSettings['swap-success']) {
successSound.play()
}
notify({
type: 'success',
title: 'Transaction successful',
txid: tx,
})
}
} catch (e) {
console.error('Place trade error:', e)
if (!isMangoError(e)) return
notify({
title: 'There was an issue.',
description: e.message,
txid: e?.txid,
type: 'error',
})
}
},
[serumOrPerpMarket]
)
const onKeyDown = useCallback(
(keyName: string) => {
const orderDetails = hotKeys.find(
(hk: HotKey) => hk.keySequence === keyName
)
if (orderDetails) {
handlePlaceOrder(orderDetails)
}
},
[handlePlaceOrder, hotKeys]
)
const showHotKeys =
hotKeys.length &&
asPath.includes('/trade') &&
mangoAccountAddress &&
!isUnownedAccount
return showHotKeys ? (
<Hotkeys
keyName={hotKeys.map((k: HotKey) => k.keySequence).toString()}
onKeyDown={onKeyDown}
>
{children}
</Hotkeys>
) : (
<>{children}</>
)
}
export default TradeHotKeys

View File

@ -65,6 +65,7 @@
"react-dom": "18.2.0",
"react-flip-numbers": "3.0.5",
"react-grid-layout": "1.3.4",
"react-hot-keys": "2.7.2",
"react-nice-dates": "3.1.0",
"react-number-format": "4.9.2",
"react-tsparticles": "2.2.4",

View File

@ -17,6 +17,7 @@ export async function getStaticProps({ locale }: { locale: string }) {
'profile',
'search',
'settings',
'trade',
])),
},
}

View File

@ -1,7 +1,11 @@
{
"above": "Above",
"animations": "Animations",
"at": "at",
"avocado": "Avocado",
"banana": "Banana",
"base-key": "Base Key",
"below": "Below",
"blueberry": "Blueberry",
"bottom-left": "Bottom-Left",
"bottom-right": "Bottom-Right",
@ -16,18 +20,40 @@
"custom": "Custom",
"dark": "Dark",
"display": "Display",
"error-alphanumeric-only": "Alphanumeric characters only",
"error-key-in-use": "Hot key already in use. Choose a unique key",
"error-key-limit-reached": "You've reached the maximum number of hot keys",
"error-must-be-above-zero": "Must be greater than zero",
"error-must-be-below-100": "Must be below 100",
"error-must-be-number": "Must be a number",
"error-order-exceeds-max": "Order exceeds max size",
"error-required-field": "This field is required",
"error-too-many-characters": "Enter one alphanumeric character",
"english": "English",
"high-contrast": "High Contrast",
"hot-keys": "Hot Keys",
"hot-keys-desc": "Use hot keys to place orders on the trade page. They execute on the market you're viewing and are not market specific.",
"key-sequence": "Key Sequence",
"language": "Language",
"light": "Light",
"lychee": "Lychee",
"mango": "Mango",
"mango-classic": "Mango Classic",
"medium": "Medium",
"new-hot-key": "New Hot Key",
"no-hot-keys": "Create your first hot key",
"notification-position": "Notification Position",
"notional": "Notional",
"number-scroll": "Number Scroll",
"olive": "Olive",
"options": "Options",
"oracle": "Oracle",
"orderbook-flash": "Orderbook Flash",
"order-side": "Order Side",
"order-size-type": "Order Size Type",
"percentage": "Percentage",
"percentage-of-max": "{{size}}% of Max",
"placing-order": "Placing Order...",
"preferred-explorer": "Preferred Explorer",
"recent-trades": "Recent Trades",
"rpc": "RPC",
@ -35,6 +61,7 @@
"rpc-url": "Enter RPC URL",
"russian": "Русский",
"save": "Save",
"save-hot-key": "Save Hot Key",
"slider": "Slider",
"solana-beach": "Solana Beach",
"solana-explorer": "Solana Explorer",
@ -45,6 +72,9 @@
"swap-success": "Swap/Trade Success",
"swap-trade-size-selector": "Swap/Trade Size Selector",
"theme": "Theme",
"tooltip-hot-key-notional-size": "Set size as a USD value.",
"tooltip-hot-key-percentage-size": "Set size as a percentage of your max leverage.",
"tooltip-hot-key-price": "Set a price as a percentage change from the oracle price.",
"top-left": "Top-Left",
"top-right": "Top-Right",
"trade-layout": "Trade Layout",
@ -52,6 +82,7 @@
"transaction-success": "Transaction Success",
"trade-chart": "Trade Chart",
"trading-view": "Trading View",
"trigger-key": "Trigger Key",
"notifications": "Notifications",
"limit-order-filled": "Limit Order Fills",
"orderbook-bandwidth-saving": "Orderbook Bandwidth Saving",

View File

@ -36,6 +36,7 @@
"maker": "Maker",
"maker-fee": "Maker Fee",
"margin": "Margin",
"market": "Market",
"market-details": "{{market}} Market Details",
"max-leverage": "Max Leverage",
"min-order-size": "Min Order Size",
@ -65,6 +66,7 @@
"price-provided-by": "Oracle by",
"quote": "Quote",
"realized-pnl": "Realized PnL",
"reduce": "Reduce",
"reduce-only": "Reduce Only",
"sells": "Sells",
"settle-funds": "Settle Funds",

View File

@ -1,7 +1,11 @@
{
"above": "Above",
"animations": "Animations",
"at": "at",
"avocado": "Avocado",
"banana": "Banana",
"base-key": "Base Key",
"below": "Below",
"blueberry": "Blueberry",
"bottom-left": "Bottom-Left",
"bottom-right": "Bottom-Right",
@ -16,18 +20,40 @@
"custom": "Custom",
"dark": "Dark",
"display": "Display",
"error-alphanumeric-only": "Alphanumeric characters only",
"error-key-in-use": "Hot key already in use. Choose a unique key",
"error-key-limit-reached": "You've reached the maximum number of hot keys",
"error-must-be-above-zero": "Must be greater than zero",
"error-must-be-below-100": "Must be below 100",
"error-must-be-number": "Must be a number",
"error-order-exceeds-max": "Order exceeds max size",
"error-required-field": "This field is required",
"error-too-many-characters": "Enter one alphanumeric character",
"english": "English",
"high-contrast": "High Contrast",
"hot-keys": "Hot Keys",
"hot-keys-desc": "Use hot keys to place orders on the trade page. They execute on the market you're viewing and are not market specific.",
"key-sequence": "Key Sequence",
"language": "Language",
"light": "Light",
"lychee": "Lychee",
"mango": "Mango",
"mango-classic": "Mango Classic",
"medium": "Medium",
"new-hot-key": "New Hot Key",
"no-hot-keys": "Create your first hot key",
"notification-position": "Notification Position",
"notional": "Notional",
"number-scroll": "Number Scroll",
"olive": "Olive",
"options": "Options",
"oracle": "Oracle",
"orderbook-flash": "Orderbook Flash",
"order-side": "Order Side",
"order-size-type": "Order Size Type",
"percentage": "Percentage",
"percentage-of-max": "{{size}}% of Max",
"placing-order": "Placing Order...",
"preferred-explorer": "Preferred Explorer",
"recent-trades": "Recent Trades",
"rpc": "RPC",
@ -35,6 +61,7 @@
"rpc-url": "Enter RPC URL",
"russian": "Русский",
"save": "Save",
"save-hot-key": "Save Hot Key",
"slider": "Slider",
"solana-beach": "Solana Beach",
"solana-explorer": "Solana Explorer",
@ -45,6 +72,9 @@
"swap-success": "Swap/Trade Success",
"swap-trade-size-selector": "Swap/Trade Size Selector",
"theme": "Theme",
"tooltip-hot-key-notional-size": "Set size as a USD value.",
"tooltip-hot-key-percentage-size": "Set size as a percentage of your max leverage.",
"tooltip-hot-key-price": "Set a price as a percentage change from the oracle price.",
"top-left": "Top-Left",
"top-right": "Top-Right",
"trade-layout": "Trade Layout",
@ -52,6 +82,7 @@
"transaction-success": "Transaction Success",
"trade-chart": "Trade Chart",
"trading-view": "Trading View",
"trigger-key": "Trigger Key",
"notifications": "Notifications",
"limit-order-filled": "Limit Order Fills",
"orderbook-bandwidth-saving": "Orderbook Bandwidth Saving",

View File

@ -36,6 +36,7 @@
"maker": "Maker",
"maker-fee": "Maker Fee",
"margin": "Margin",
"market": "Market",
"market-details": "{{market}} Market Details",
"max-leverage": "Max Leverage",
"min-order-size": "Min Order Size",
@ -65,6 +66,7 @@
"price-provided-by": "Oracle by",
"quote": "Quote",
"realized-pnl": "Realized PnL",
"reduce": "Reduce",
"reduce-only": "Reduce Only",
"sells": "Sells",
"settle-funds": "Settle Funds",

View File

@ -1,7 +1,11 @@
{
"above": "Above",
"animations": "Animations",
"at": "at",
"avocado": "Avocado",
"banana": "Banana",
"base-key": "Base Key",
"below": "Below",
"blueberry": "Blueberry",
"bottom-left": "Bottom-Left",
"bottom-right": "Bottom-Right",
@ -16,18 +20,40 @@
"custom": "Custom",
"dark": "Dark",
"display": "Display",
"error-alphanumeric-only": "Alphanumeric characters only",
"error-key-in-use": "Hot key already in use. Choose a unique key",
"error-key-limit-reached": "You've reached the maximum number of hot keys",
"error-must-be-above-zero": "Must be greater than zero",
"error-must-be-below-100": "Must be below 100",
"error-must-be-number": "Must be a number",
"error-order-exceeds-max": "Order exceeds max size",
"error-required-field": "This field is required",
"error-too-many-characters": "Enter one alphanumeric character",
"english": "English",
"high-contrast": "High Contrast",
"hot-keys": "Hot Keys",
"hot-keys-desc": "Use hot keys to place orders on the trade page. They execute on the market you're viewing and are not market specific.",
"key-sequence": "Key Sequence",
"language": "Language",
"light": "Light",
"lychee": "Lychee",
"mango": "Mango",
"mango-classic": "Mango Classic",
"medium": "Medium",
"new-hot-key": "New Hot Key",
"no-hot-keys": "Create your first hot key",
"notification-position": "Notification Position",
"notional": "Notional",
"number-scroll": "Number Scroll",
"olive": "Olive",
"options": "Options",
"oracle": "Oracle",
"orderbook-flash": "Orderbook Flash",
"order-side": "Order Side",
"order-size-type": "Order Size Type",
"percentage": "Percentage",
"percentage-of-max": "{{size}}% of Max",
"placing-order": "Placing Order...",
"preferred-explorer": "Preferred Explorer",
"recent-trades": "Recent Trades",
"rpc": "RPC",
@ -35,6 +61,7 @@
"rpc-url": "Enter RPC URL",
"russian": "Русский",
"save": "Save",
"save-hot-key": "Save Hot Key",
"slider": "Slider",
"solana-beach": "Solana Beach",
"solana-explorer": "Solana Explorer",
@ -45,6 +72,9 @@
"swap-success": "Swap/Trade Success",
"swap-trade-size-selector": "Swap/Trade Size Selector",
"theme": "Theme",
"tooltip-hot-key-notional-size": "Set size as a USD value.",
"tooltip-hot-key-percentage-size": "Set size as a percentage of your max leverage.",
"tooltip-hot-key-price": "Set a price as a percentage change from the oracle price.",
"top-left": "Top-Left",
"top-right": "Top-Right",
"trade-layout": "Trade Layout",
@ -52,6 +82,7 @@
"transaction-success": "Transaction Success",
"trade-chart": "Trade Chart",
"trading-view": "Trading View",
"trigger-key": "Trigger Key",
"notifications": "Notifications",
"limit-order-filled": "Limit Order Fills",
"orderbook-bandwidth-saving": "Orderbook Bandwidth Saving",

View File

@ -36,6 +36,7 @@
"maker": "Maker",
"maker-fee": "Maker Fee",
"margin": "Margin",
"market": "Market",
"market-details": "{{market}} Market Details",
"max-leverage": "Max Leverage",
"min-order-size": "Min Order Size",
@ -65,6 +66,7 @@
"price-provided-by": "Oracle by",
"quote": "Quote",
"realized-pnl": "Realized PnL",
"reduce": "Reduce",
"reduce-only": "Reduce Only",
"sells": "Sells",
"settle-funds": "Settle Funds",

View File

@ -1,7 +1,11 @@
{
"above": "Above",
"animations": "动画",
"at": "at",
"avocado": "酪梨",
"banana": "香蕉",
"base-key": "Base Key",
"below": "Below",
"blueberry": "蓝莓",
"bottom-left": "左下",
"bottom-right": "右下",
@ -16,8 +20,20 @@
"custom": "自定",
"dark": "暗",
"display": "显示",
"error-alphanumeric-only": "Alphanumeric characters only",
"error-key-in-use": "Hot key already in use. Choose a unique key",
"error-key-limit-reached": "You've reached the maximum number of hot keys",
"error-must-be-above-zero": "Must be greater than zero",
"error-must-be-below-100": "Must be below 100",
"error-must-be-number": "Must be a number",
"error-order-exceeds-max": "Order exceeds max size",
"error-required-field": "This field is required",
"error-too-many-characters": "Enter one alphanumeric character",
"english": "English",
"high-contrast": "高对比度",
"hot-keys": "Hot Keys",
"hot-keys-desc": "Use hot keys to place orders on the trade page. They execute on the market you're viewing and are not market specific.",
"key-sequence": "Key Sequence",
"language": "语言",
"light": "光",
"limit-order-filled": "限价单成交",
@ -25,11 +41,21 @@
"mango": "芒果",
"mango-classic": "芒果经典",
"medium": "中",
"new-hot-key": "New Hot Key",
"no-hot-keys": "Create your first hot key",
"notification-position": "通知位置",
"notional": "Notional",
"notifications": "通知",
"number-scroll": "数字滑动",
"olive": "橄榄",
"options": "Options",
"oracle": "Oracle",
"orderbook-flash": "挂单薄闪光",
"order-side": "Order Side",
"order-size-type": "Order Size Type",
"percentage": "Percentage",
"percentage-of-max": "{{size}}% of Max",
"placing-order": "Placing Order...",
"preferred-explorer": "首选探索器",
"recent-trades": "最近交易",
"rpc": "RPC",
@ -37,6 +63,7 @@
"rpc-url": "输入RPC URL",
"russian": "Русский",
"save": "存",
"save-hot-key": "Save Hot Key",
"sign-to-notifications": "登录通知中心以更改设置",
"slider": "滑块",
"solana-beach": "Solana Beach",
@ -48,11 +75,15 @@
"swap-success": "换币/交易成功",
"swap-trade-size-selector": "换币/交易大小选择器",
"theme": "模式",
"tooltip-hot-key-notional-size": "Set size as a USD value.",
"tooltip-hot-key-percentage-size": "Set size as a percentage of your max leverage.",
"tooltip-hot-key-price": "Set a price as a percentage change from the oracle price.",
"top-left": "左上",
"top-right": "右上",
"trade-chart": "交易图表",
"trade-layout": "交易布局",
"trading-view": "Trading View",
"trigger-key": "Trigger Key",
"transaction-fail": "交易失败",
"transaction-success": "交易成功",
"orderbook-bandwidth-saving": "Orderbook Bandwidth Saving",

View File

@ -31,14 +31,15 @@
"insured": "{{token}} Insured",
"last-updated": "Last updated",
"limit": "Limit",
"limit-price": "Limit Price",
"long": "Long",
"maker": "Maker",
"limit-price": "限价价格",
"long": "做多",
"maker": "挂单者",
"margin": "保证金",
"market": "Market",
"market-details": "{{market}}市场细节",
"max-leverage": "最多杠杆",
"min-order-size": "最小订单量",
"maker-fee": "Maker Fee",
"margin": "Margin",
"market-details": "{{market}} Market Details",
"max-leverage": "Max Leverage",
"min-order-size": "Min Order Size",
"min-order-size-error": "Min order size is {{minSize}} {{symbol}}",
"more-details": "More Details",
"no-balances": "No balances",
@ -60,25 +61,25 @@
"placing-order": "Placing Order",
"positions": "Positions",
"post": "Post",
"preview-sound": "Preview Sound",
"price-expect": "The price you receive may be worse than you expect and full execution is not guaranteed. Max slippage is 2.5% for your safety. The part of your position with slippage beyond 2.5% will not be closed.",
"price-provided-by": "Oracle by",
"quote": "Quote",
"realized-pnl": "Realized PnL",
"reduce-only": "Reduce Only",
"sells": "Sells",
"settle-funds": "Settle Funds",
"settle-funds-error": "Failed to settle funds",
"short": "Short",
"show-asks": "Show Asks",
"show-bids": "Show Bids",
"side": "Side",
"size": "Size",
"spread": "Spread",
"stable-price": "Stable Price",
"taker": "Taker",
"preview-sound": "声音预览",
"price-expect": "您收到的价格可能与您预期有差异,并且无法保证完全执行。为了您的安全,最大滑点保持为 2.5%。超过 2.5%滑点的部分不会被平仓。",
"price-provided-by": "语言机来自",
"quote": "计价",
"reduce": "Reduce",
"reduce-only": "限减少",
"sells": "卖单",
"settle-funds": "借清资金",
"settle-funds-error": "借清出错",
"short": "做空",
"show-asks": "显示要价",
"show-bids": "显示出价",
"side": "方向",
"size": "數量",
"spread": "差價",
"stable-price": "穩定價格",
"taker": "吃單者",
"tick-size": "波動單位",
"taker-fee": "Taker Fee",
"tick-size": "Tick Size",
"tooltip-borrow-balance": "You'll use your {{balance}} {{token}} balance and borrow {{borrowAmount}} {{token}} to execute this trade. The current {{token}} variable borrow rate is {{rate}}%",
"tooltip-borrow-no-balance": "You'll borrow {{borrowAmount}} {{token}} to execute this trade. The current {{token}} variable borrow rate is {{rate}}%",
"tooltip-enable-margin": "Enable spot margin for this trade",

View File

@ -1,7 +1,11 @@
{
"above": "Above",
"animations": "動畫",
"at": "at",
"avocado": "酪梨",
"banana": "香蕉",
"base-key": "Base Key",
"below": "Below",
"blueberry": "藍莓",
"bottom-left": "左下",
"bottom-right": "右下",
@ -16,8 +20,20 @@
"custom": "自定",
"dark": "暗",
"display": "顯示",
"error-alphanumeric-only": "Alphanumeric characters only",
"error-key-in-use": "Hot key already in use. Choose a unique key",
"error-key-limit-reached": "You've reached the maximum number of hot keys",
"error-must-be-above-zero": "Must be greater than zero",
"error-must-be-below-100": "Must be below 100",
"error-must-be-number": "Must be a number",
"error-order-exceeds-max": "Order exceeds max size",
"error-required-field": "This field is required",
"error-too-many-characters": "Enter one alphanumeric character",
"english": "English",
"high-contrast": "高對比度",
"hot-keys": "Hot Keys",
"hot-keys-desc": "Use hot keys to place orders on the trade page. They execute on the market you're viewing and are not market specific.",
"key-sequence": "Key Sequence",
"language": "語言",
"light": "光",
"limit-order-filled": "限价单成交",
@ -25,11 +41,21 @@
"mango": "芒果",
"mango-classic": "芒果經典",
"medium": "中",
"new-hot-key": "New Hot Key",
"no-hot-keys": "Create your first hot key",
"notification-position": "通知位置",
"notional": "Notional",
"notifications": "通知",
"number-scroll": "數字滑動",
"olive": "橄欖",
"options": "Options",
"oracle": "Oracle",
"orderbook-flash": "掛單薄閃光",
"order-side": "Order Side",
"order-size-type": "Order Size Type",
"percentage": "Percentage",
"percentage-of-max": "{{size}}% of Max",
"placing-order": "Placing Order...",
"preferred-explorer": "首選探索器",
"recent-trades": "最近交易",
"rpc": "RPC",
@ -37,6 +63,7 @@
"rpc-url": "輸入RPC URL",
"russian": "Русский",
"save": "存",
"save-hot-key": "Save Hot Key",
"sign-to-notifications": "登录通知中心以更改设置",
"slider": "滑塊",
"solana-beach": "Solana Beach",
@ -48,11 +75,15 @@
"swap-success": "換幣/交易成功",
"swap-trade-size-selector": "換幣/交易大小選擇器",
"theme": "模式",
"tooltip-hot-key-notional-size": "Set size as a USD value.",
"tooltip-hot-key-percentage-size": "Set size as a percentage of your max leverage.",
"tooltip-hot-key-price": "Set a price as a percentage change from the oracle price.",
"top-left": "左上",
"top-right": "右上",
"trade-chart": "交易圖表",
"trade-layout": "交易佈局",
"trading-view": "Trading View",
"trigger-key": "Trigger Key",
"transaction-fail": "交易失敗",
"transaction-success": "交易成功",
"orderbook-bandwidth-saving": "Orderbook Bandwidth Saving",

View File

@ -31,14 +31,15 @@
"insured": "{{token}} Insured",
"last-updated": "Last updated",
"limit": "Limit",
"limit-price": "Limit Price",
"long": "Long",
"maker": "Maker",
"maker-fee": "Maker Fee",
"margin": "Margin",
"market-details": "{{market}} Market Details",
"max-leverage": "Max Leverage",
"min-order-size": "Min Order Size",
"limit-price": "限價價格",
"long": "做多",
"maker": "掛單者",
"maker-fee": "掛單者 Fee",
"margin": "保證金",
"market": "Market",
"market-details": "{{market}}市場細節",
"max-leverage": "最多槓桿",
"min-order-size": "最小訂單量",
"min-order-size-error": "Min order size is {{minSize}} {{symbol}}",
"more-details": "More Details",
"no-balances": "No balances",
@ -60,25 +61,26 @@
"placing-order": "Placing Order",
"positions": "Positions",
"post": "Post",
"preview-sound": "Preview Sound",
"price-expect": "The price you receive may be worse than you expect and full execution is not guaranteed. Max slippage is 2.5% for your safety. The part of your position with slippage beyond 2.5% will not be closed.",
"price-provided-by": "Oracle by",
"quote": "Quote",
"preview-sound": "聲音預覽",
"price-expect": "您收到的價格可能與您預期有差異,並且無法保證完全執行。為了您的安全,最大滑點保持為 2.5%。超過 2.5%滑點的部分不會被平倉。",
"price-provided-by": "語言機來自",
"quote": "計價",
"reduce": "Reduce",
"reduce-only": "限減少",
"sells": "賣單",
"settle-funds": "借清資金",
"settle-funds-error": "借清出錯",
"short": "做空",
"show-asks": "顯示要價",
"show-bids": "顯示出價",
"side": "方向",
"size": "數量",
"spread": "差價",
"stable-price": "穩定價格",
"taker": "吃單者",
"taker-fee": "吃單者 Fee",
"tick-size": "波動單位",
"realized-pnl": "Realized PnL",
"reduce-only": "Reduce Only",
"sells": "Sells",
"settle-funds": "Settle Funds",
"settle-funds-error": "Failed to settle funds",
"short": "Short",
"show-asks": "Show Asks",
"show-bids": "Show Bids",
"side": "Side",
"size": "Size",
"spread": "Spread",
"stable-price": "Stable Price",
"taker": "Taker",
"taker-fee": "Taker Fee",
"tick-size": "Tick Size",
"tooltip-borrow-balance": "You'll use your {{balance}} {{token}} balance and borrow {{borrowAmount}} {{token}} to execute this trade. The current {{token}} variable borrow rate is {{rate}}%",
"tooltip-borrow-no-balance": "You'll borrow {{borrowAmount}} {{token}} to execute this trade. The current {{token}} variable borrow rate is {{rate}}%",
"tooltip-enable-margin": "Enable spot margin for this trade",

View File

@ -59,6 +59,8 @@ export const STATS_TAB_KEY = 'activeStatsTab-0.1'
export const USE_ORDERBOOK_FEED_KEY = 'useOrderbookFeed-0.1'
export const HOT_KEYS_KEY = 'hotKeys-0.1'
// Unused
export const PROFILE_CATEGORIES = [
'borrower',

View File

@ -5379,6 +5379,11 @@ hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
dependencies:
react-is "^16.7.0"
hotkeys-js@^3.8.1:
version "3.10.2"
resolved "https://registry.yarnpkg.com/hotkeys-js/-/hotkeys-js-3.10.2.tgz#cf52661904f5a13a973565cb97085fea2f5ae257"
integrity sha512-Z6vLmJTYzkbZZXlBkhrYB962Q/rZGc/WHQiyEGu9ZZVF7bAeFDjjDa31grWREuw9Ygb4zmlov2bTkPYqj0aFnQ==
howler@2.2.3:
version "2.2.3"
resolved "https://registry.yarnpkg.com/howler/-/howler-2.2.3.tgz#a2eff9b08b586798e7a2ee17a602a90df28715da"
@ -7139,6 +7144,14 @@ react-grid-layout@1.3.4:
react-draggable "^4.0.0"
react-resizable "^3.0.4"
react-hot-keys@2.7.2:
version "2.7.2"
resolved "https://registry.yarnpkg.com/react-hot-keys/-/react-hot-keys-2.7.2.tgz#7d2b02b7e2cf69182ea71ca01885446ebfae01d2"
integrity sha512-Z7eSh7SU6s52+zP+vkfFoNk0x4kgEmnwqDiyACKv53crK2AZ7FUaBLnf+vxLor3dvtId9murLmKOsrJeYgeHWw==
dependencies:
hotkeys-js "^3.8.1"
prop-types "^15.7.2"
react-i18next@^11.18.0:
version "11.18.6"
resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.18.6.tgz#e159c2960c718c1314f1e8fcaa282d1c8b167887"