[wip] add trigger order components
This commit is contained in:
parent
5da4887736
commit
8abbce0238
|
@ -11,6 +11,7 @@ import { floorToDecimal } from '../utils/index'
|
|||
import useMangoStore from '../stores/useMangoStore'
|
||||
import Button from './Button'
|
||||
import TradeType from './TradeType'
|
||||
import TriggerType from './TriggerType'
|
||||
import Input from './Input'
|
||||
import Switch from './Switch'
|
||||
import { Market } from '@project-serum/serum'
|
||||
|
@ -35,9 +36,21 @@ export default function TradeForm() {
|
|||
const mangoAccount = useMangoStore((s) => s.selectedMangoAccount.current)
|
||||
const mangoClient = useMangoStore((s) => s.connection.client)
|
||||
const market = useMangoStore((s) => s.selectedMarket.current)
|
||||
const { side, baseSize, quoteSize, price, tradeType } = useMangoStore(
|
||||
(s) => s.tradeForm
|
||||
)
|
||||
const isPerpMarket = market instanceof PerpMarket
|
||||
|
||||
const {
|
||||
side,
|
||||
baseSize,
|
||||
quoteSize,
|
||||
price,
|
||||
tradeType,
|
||||
triggerPrice,
|
||||
triggerType,
|
||||
} = useMangoStore((s) => s.tradeForm)
|
||||
const isLimitOrder = ['Limit', 'Trigger Limit'].includes(tradeType)
|
||||
const isMarketOrder = ['Market', 'Trigger Market'].includes(tradeType)
|
||||
const isTriggerOrder = ['Trigger Limit', 'Trigger Market'].includes(tradeType)
|
||||
|
||||
const { width } = useViewport()
|
||||
const isMobile = width ? width < breakpoints.sm : false
|
||||
|
||||
|
@ -102,6 +115,24 @@ export default function TradeForm() {
|
|||
s.tradeForm.tradeType = type
|
||||
})
|
||||
|
||||
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 setTriggerType = (type) =>
|
||||
set((s) => {
|
||||
s.tradeForm.triggerType = type
|
||||
})
|
||||
|
||||
const markPriceRef = useRef(useMangoStore.getState().selectedMarket.markPrice)
|
||||
const markPrice = markPriceRef.current
|
||||
useEffect(
|
||||
|
@ -131,7 +162,7 @@ export default function TradeForm() {
|
|||
let tickSize = 1
|
||||
if (market instanceof Market) {
|
||||
tickSize = market.tickSize
|
||||
} else if (market instanceof PerpMarket) {
|
||||
} else if (isPerpMarket) {
|
||||
const baseDecimals = getTokenBySymbol(
|
||||
groupConfig,
|
||||
marketConfig.baseSymbol
|
||||
|
@ -179,7 +210,7 @@ export default function TradeForm() {
|
|||
return
|
||||
}
|
||||
|
||||
if (!Number(price) && tradeType === 'Limit') {
|
||||
if (!Number(price) && isLimitOrder) {
|
||||
setBaseSize('')
|
||||
return
|
||||
}
|
||||
|
@ -191,9 +222,11 @@ export default function TradeForm() {
|
|||
|
||||
const onTradeTypeChange = (tradeType) => {
|
||||
setTradeType(tradeType)
|
||||
if (tradeType === 'Market') {
|
||||
if (isMarketOrder) {
|
||||
setIoc(true)
|
||||
setPrice('')
|
||||
if (isTriggerOrder) {
|
||||
setPrice(triggerPrice)
|
||||
}
|
||||
} else {
|
||||
const priceOnBook = side === 'buy' ? orderbook?.asks : orderbook?.bids
|
||||
if (priceOnBook && priceOnBook.length > 0 && priceOnBook[0].length > 0) {
|
||||
|
@ -217,7 +250,7 @@ export default function TradeForm() {
|
|||
}
|
||||
|
||||
async function onSubmit() {
|
||||
if (!price && tradeType === 'Limit') {
|
||||
if (!price && isLimitOrder) {
|
||||
notify({
|
||||
title: 'Missing price',
|
||||
type: 'error',
|
||||
|
@ -245,7 +278,8 @@ export default function TradeForm() {
|
|||
orderbook,
|
||||
baseSize,
|
||||
side,
|
||||
price
|
||||
price,
|
||||
triggerPrice
|
||||
)
|
||||
|
||||
if (!orderPrice) {
|
||||
|
@ -305,7 +339,7 @@ export default function TradeForm() {
|
|||
}
|
||||
|
||||
const disabledTradeButton =
|
||||
(!price && tradeType === 'Limit') ||
|
||||
(!price && isLimitOrder) ||
|
||||
!baseSize ||
|
||||
!connected ||
|
||||
submitting ||
|
||||
|
@ -353,7 +387,7 @@ export default function TradeForm() {
|
|||
step={tickSize}
|
||||
onChange={(e) => onSetPrice(e.target.value)}
|
||||
value={price}
|
||||
disabled={tradeType === 'Market'}
|
||||
disabled={isMarketOrder}
|
||||
prefix={'Price'}
|
||||
suffix={groupConfig.quoteSymbol}
|
||||
className="rounded-r-none"
|
||||
|
@ -362,6 +396,7 @@ export default function TradeForm() {
|
|||
<TradeType
|
||||
onChange={onTradeTypeChange}
|
||||
value={tradeType}
|
||||
offerTriggers={isPerpMarket}
|
||||
className="hover:border-th-primary flex-grow"
|
||||
/>
|
||||
</Input.Group>
|
||||
|
@ -402,10 +437,11 @@ export default function TradeForm() {
|
|||
orderbook,
|
||||
baseSize ? baseSize : 0,
|
||||
side,
|
||||
price
|
||||
price,
|
||||
triggerPrice
|
||||
)}
|
||||
/>
|
||||
{tradeType !== 'Market' ? (
|
||||
{isLimitOrder && (
|
||||
<div className="flex mt-2">
|
||||
<Switch checked={postOnly} onChange={postOnChange}>
|
||||
POST
|
||||
|
@ -416,7 +452,30 @@ export default function TradeForm() {
|
|||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
)}
|
||||
|
||||
{isTriggerOrder && (
|
||||
<Input.Group className="mt-4">
|
||||
<TriggerType
|
||||
onChange={setTriggerType}
|
||||
value={triggerType}
|
||||
className="hover:border-th-primary flex-grow"
|
||||
/>
|
||||
|
||||
<Input
|
||||
type="number"
|
||||
min="0"
|
||||
step={tickSize}
|
||||
onChange={(e) => setTriggerPrice(e.target.value)}
|
||||
value={triggerPrice}
|
||||
prefix={'Price'}
|
||||
suffix={groupConfig.quoteSymbol}
|
||||
className="rounded-l-none"
|
||||
wrapperClassName="rounded-l-none w-3/5"
|
||||
/>
|
||||
</Input.Group>
|
||||
)}
|
||||
|
||||
<div className={`flex py-4`}>
|
||||
{ipAllowed ? (
|
||||
side === 'buy' ? (
|
||||
|
@ -435,9 +494,7 @@ export default function TradeForm() {
|
|||
</div>
|
||||
) : (
|
||||
`${baseSize > 0 ? 'Buy ' + baseSize : 'Buy '} ${
|
||||
marketConfig.name.includes('PERP')
|
||||
? marketConfig.name
|
||||
: marketConfig.baseSymbol
|
||||
isPerpMarket ? marketConfig.name : marketConfig.baseSymbol
|
||||
}`
|
||||
)}
|
||||
</Button>
|
||||
|
@ -457,9 +514,7 @@ export default function TradeForm() {
|
|||
</div>
|
||||
) : (
|
||||
`${baseSize > 0 ? 'Sell ' + baseSize : 'Sell '} ${
|
||||
marketConfig.name.includes('PERP')
|
||||
? marketConfig.name
|
||||
: marketConfig.baseSymbol
|
||||
isPerpMarket ? marketConfig.name : marketConfig.baseSymbol
|
||||
}`
|
||||
)}
|
||||
</Button>
|
||||
|
@ -582,7 +637,8 @@ export default function TradeForm() {
|
|||
orderbook,
|
||||
baseSize ? baseSize : 0,
|
||||
side,
|
||||
price
|
||||
price,
|
||||
triggerPrice
|
||||
)}
|
||||
/>
|
||||
{tradeType !== 'Market' ? (
|
||||
|
|
|
@ -8,11 +8,16 @@ const StyledListbox = styled(Listbox.Button)`
|
|||
border-left: 1px solid transparent;
|
||||
`
|
||||
|
||||
const TRADE_TYPES = ['Limit', 'Market']
|
||||
|
||||
const TradeType = ({ value, onChange, className = '' }) => {
|
||||
|
||||
const TradeType = ({ value, onChange, offerTriggers = false, className = '' }) => {
|
||||
const { width } = useViewport()
|
||||
const isMobile = width ? width < breakpoints.sm : false
|
||||
|
||||
const TRADE_TYPES = ['Limit', 'Market']
|
||||
if (offerTriggers)
|
||||
TRADE_TYPES.push('Trigger Market', 'Trigger Limit')
|
||||
|
||||
return (
|
||||
<div className={`relative ${className}`}>
|
||||
{!isMobile ? (
|
||||
|
@ -60,30 +65,20 @@ const TradeType = ({ value, onChange, className = '' }) => {
|
|||
</Listbox>
|
||||
) : (
|
||||
<div className="flex">
|
||||
<div
|
||||
{ TRADE_TYPES.map( tradeType => {
|
||||
<div
|
||||
className={`px-2 py-1 ml-2 rounded-md cursor-pointer default-transition bg-th-bkg-4
|
||||
${
|
||||
value === 'Limit'
|
||||
value === tradeType
|
||||
? `ring-1 ring-inset ring-th-primary text-th-primary`
|
||||
: `text-th-fgd-1 opacity-50 hover:opacity-100`
|
||||
}
|
||||
`}
|
||||
onClick={() => onChange('Limit')}
|
||||
onClick={() => onChange(tradeType)}
|
||||
>
|
||||
Limit
|
||||
</div>
|
||||
<div
|
||||
className={`px-2 py-1 ml-2 rounded-md cursor-pointer default-transition bg-th-bkg-4
|
||||
${
|
||||
value === 'Market'
|
||||
? `ring-1 ring-inset ring-th-primary text-th-primary`
|
||||
: `text-th-fgd-1 opacity-50 hover:opacity-100`
|
||||
}
|
||||
`}
|
||||
onClick={() => onChange('Market')}
|
||||
>
|
||||
Market
|
||||
{tradeType}
|
||||
</div>
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
import { Listbox } from '@headlessui/react'
|
||||
import styled from '@emotion/styled'
|
||||
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/solid'
|
||||
import { useViewport } from '../hooks/useViewport'
|
||||
import { breakpoints } from './TradePageGrid'
|
||||
|
||||
const StyledListbox = styled(Listbox.Button)`
|
||||
border-right: 1px solid transparent;
|
||||
`
|
||||
|
||||
const TriggerType = ({ value, onChange, className = '' }) => {
|
||||
const { width } = useViewport()
|
||||
const isMobile = width ? width < breakpoints.sm : false
|
||||
|
||||
const TRIGGER_TYPES = ['Above', 'Below']
|
||||
|
||||
return (
|
||||
<div className={`relative ${className}`}>
|
||||
{!isMobile ? (
|
||||
<Listbox value={value} onChange={onChange}>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<StyledListbox
|
||||
className={`font-normal h-full w-full bg-th-bkg-1 border border-th-fgd-4 hover:border-th-primary rounded rounded-r-none focus:outline-none focus:border-th-primary`}
|
||||
>
|
||||
<div
|
||||
className={`flex items-center justify-between space-x-4 pl-2 pr-1`}
|
||||
>
|
||||
<span>{value}</span>
|
||||
{open ? (
|
||||
<ChevronUpIcon className={`h-5 w-5 mr-1 text-th-primary`} />
|
||||
) : (
|
||||
<ChevronDownIcon
|
||||
className={`h-5 w-5 mr-1 text-th-primary`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</StyledListbox>
|
||||
{open ? (
|
||||
<Listbox.Options
|
||||
static
|
||||
className={`z-20 w-full p-1 absolute left-0 mt-1 bg-th-bkg-1 origin-top-left divide-y divide-th-bkg-3 shadow-lg outline-none rounded-md`}
|
||||
>
|
||||
{TRIGGER_TYPES.map((type) => (
|
||||
<Listbox.Option key={type} value={type}>
|
||||
{({ selected }) => (
|
||||
<div
|
||||
className={`p-2 hover:bg-th-bkg-2 hover:cursor-pointer tracking-wider ${
|
||||
selected && `text-th-primary`
|
||||
}`}
|
||||
>
|
||||
{type}
|
||||
</div>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</Listbox>
|
||||
) : (
|
||||
<div className="flex">
|
||||
{ TRIGGER_TYPES.map( triggerType => {
|
||||
<div
|
||||
className={`px-2 py-1 ml-2 rounded-md cursor-pointer default-transition bg-th-bkg-4
|
||||
${
|
||||
value === triggerType
|
||||
? `ring-1 ring-inset ring-th-primary text-th-primary`
|
||||
: `text-th-fgd-1 opacity-50 hover:opacity-100`
|
||||
}
|
||||
`}
|
||||
onClick={() => onChange(triggerType)}
|
||||
>
|
||||
{triggerType}
|
||||
</div>
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TriggerType
|
|
@ -144,7 +144,9 @@ interface MangoStore extends State {
|
|||
price: number | ''
|
||||
baseSize: number | ''
|
||||
quoteSize: number | ''
|
||||
tradeType: 'Market' | 'Limit'
|
||||
tradeType: 'Market' | 'Limit' | 'Trigger Market' | 'Trigger Limit'
|
||||
triggerPrice: number | ''
|
||||
triggerType: 'Above' | 'Below'
|
||||
}
|
||||
wallet: {
|
||||
providerUrl: string
|
||||
|
@ -215,6 +217,8 @@ const useMangoStore = create<MangoStore>((set, get) => {
|
|||
quoteSize: '',
|
||||
tradeType: 'Limit',
|
||||
price: '',
|
||||
triggerPrice: '',
|
||||
triggerType: 'Above',
|
||||
},
|
||||
wallet: INITIAL_STATE.WALLET,
|
||||
settings: {
|
||||
|
|
|
@ -101,10 +101,13 @@ export function calculateTradePrice(
|
|||
orderBook: Orderbook,
|
||||
baseSize: number,
|
||||
side: 'buy' | 'sell',
|
||||
price: string | number
|
||||
price: string | number,
|
||||
triggerPrice?: string | number
|
||||
): number {
|
||||
if (tradeType === 'Market') {
|
||||
return calculateMarketPrice(orderBook, baseSize, side)
|
||||
} else if (tradeType === 'Trigger Market') {
|
||||
return Number(triggerPrice)
|
||||
}
|
||||
return Number(price)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue